diff --git a/modules/vcsrepo/.gitignore b/modules/vcsrepo/.gitignore new file mode 100644 index 0000000..ddd2d45 --- /dev/null +++ b/modules/vcsrepo/.gitignore @@ -0,0 +1,4 @@ +coverage +.DS_Store +.#* +\#* diff --git a/modules/vcsrepo/.travis.yml b/modules/vcsrepo/.travis.yml new file mode 100644 index 0000000..f349b8d --- /dev/null +++ b/modules/vcsrepo/.travis.yml @@ -0,0 +1,25 @@ +--- +branches: + only: + - master +notifications: + email: false +language: ruby +script: 'bundle exec rake spec' +after_success: +- git clone -q git://github.com/puppetlabs/ghpublisher.git .forge-releng +- .forge-releng/publish +rvm: +- 1.8.7 +- 1.9.3 +env: + matrix: + - PUPPET_VERSION=2.6.18 + - PUPPET_VERSION=2.7.21 + - PUPPET_VERSION=3.1.1 + global: + - PUBLISHER_LOGIN=puppetlabs + - secure: |- + ZiIkYd9+CdPzpwSjFPnVkCx1FIlipxpbdyD33q94h2Tj5zXjNb1GXizVy0NR + kVxGhU5Ld8y9z8DTqKRgCI1Yymg3H//OU++PKLOQj/X5juWVR4URBNPeBOzu + IJBDl1MADKA4i1+jAZPpz4mTvTtKS4pWKErgCSmhSfsY1hs7n6c= diff --git a/modules/vcsrepo/Gemfile b/modules/vcsrepo/Gemfile new file mode 100644 index 0000000..9b6706d --- /dev/null +++ b/modules/vcsrepo/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' +gem 'rake', '~> 0.8.7' +gem 'rspec', '~> 1.3' +gem 'mocha', '~> 0.12.9', :require => false +gem 'puppet', '~> 2.7' diff --git a/modules/vcsrepo/Gemfile.lock b/modules/vcsrepo/Gemfile.lock new file mode 100644 index 0000000..ce22804 --- /dev/null +++ b/modules/vcsrepo/Gemfile.lock @@ -0,0 +1,20 @@ +GEM + remote: https://rubygems.org/ + specs: + facter (1.7.2) + metaclass (0.0.1) + mocha (0.12.10) + metaclass (~> 0.0.1) + puppet (2.7.22) + facter (~> 1.5) + rake (0.8.7) + rspec (1.3.2) + +PLATFORMS + ruby + +DEPENDENCIES + mocha (~> 0.12.9) + puppet (~> 2.7) + rake (~> 0.8.7) + rspec (~> 1.3) diff --git a/modules/vcsrepo/LICENSE b/modules/vcsrepo/LICENSE new file mode 100644 index 0000000..2ee80c8 --- /dev/null +++ b/modules/vcsrepo/LICENSE @@ -0,0 +1,17 @@ +Copyright (C) 2010-2012 Puppet Labs Inc. + +Puppet Labs can be contacted at: info@puppetlabs.com + +This program and entire repository is free software; you can +redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software +Foundation; either version 2 of the License, or any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/modules/vcsrepo/Modulefile b/modules/vcsrepo/Modulefile new file mode 100644 index 0000000..5d95183 --- /dev/null +++ b/modules/vcsrepo/Modulefile @@ -0,0 +1,4 @@ +name 'puppetlabs/vcsrepo' +version '0.1.2' +summary 'Manage repositories from various version control systems' +description 'Manage repositories from various version control systems' diff --git a/modules/vcsrepo/README.BZR.markdown b/modules/vcsrepo/README.BZR.markdown new file mode 100644 index 0000000..cc257e9 --- /dev/null +++ b/modules/vcsrepo/README.BZR.markdown @@ -0,0 +1,47 @@ +Using vcsrepo with Bazaar +========================= + +To create a blank repository +---------------------------- + +Define a `vcsrepo` without a `source` or `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => bzr + } + +To branch from an existing repository +------------------------------------- + +Provide the `source` location: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => bzr, + source => 'lp:myproj' + } + +For a specific revision, use `revision` with a valid revisionspec +(see `bzr help revisionspec` for more information on formatting a revision): + + vcsrepo { "/path/to/repo": + ensure => present, + provider => bzr, + source => 'lp:myproj', + revision => 'menesis@pov.lt-20100309191856-4wmfqzc803fj300x' + } + +For sources that use SSH (eg, `bzr+ssh://...`, `sftp://...`) +------------------------------------------------------------ + +Manage your SSH keys with Puppet and use `require` in your `vcsrepo` +to ensure they are present. For more information, see the `require` +metaparameter documentation[1]. + +More Examples +------------- + +For examples you can run, see `examples/bzr/` + +[1]: http://docs.puppetlabs.com/references/stable/metaparameter.html#require diff --git a/modules/vcsrepo/README.CVS.markdown b/modules/vcsrepo/README.CVS.markdown new file mode 100644 index 0000000..10121a7 --- /dev/null +++ b/modules/vcsrepo/README.CVS.markdown @@ -0,0 +1,56 @@ +Using vcsrepo with CVS +====================== + +To create a blank repository +---------------------------- + +Define a `vcsrepo` without a `source` or `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => cvs + } + +To checkout/update from a repository +------------------------------------ + +To get the current mainline: + + vcsrepo { "/path/to/workspace": + ensure => present, + provider => cvs, + source => ":pserver:anonymous@example.com:/sources/myproj" + } + +You can use the `compression` parameter (it works like CVS `-z`): + + vcsrepo { "/path/to/workspace": + ensure => present, + provider => cvs, + compression => 3, + source => ":pserver:anonymous@example.com:/sources/myproj" + } + +For a specific tag, use `revision`: + + vcsrepo { "/path/to/workspace": + ensure => present, + provider => cvs, + compression => 3, + source => ":pserver:anonymous@example.com:/sources/myproj", + revision => "SOMETAG" + } + +For sources that use SSH +------------------------ + +Manage your SSH keys with Puppet and use `require` in your `vcsrepo` +to ensure they are present. For more information, see the `require` +metaparameter documentation[1]. + +More Examples +------------- + +For examples you can run, see `examples/cvs/` + +[1]: http://docs.puppetlabs.com/references/stable/metaparameter.html#require diff --git a/modules/vcsrepo/README.GIT.markdown b/modules/vcsrepo/README.GIT.markdown new file mode 100644 index 0000000..846bdcc --- /dev/null +++ b/modules/vcsrepo/README.GIT.markdown @@ -0,0 +1,95 @@ +Using vcsrepo with Git +====================== + +To create a blank repository +---------------------------- + +Define a `vcsrepo` without a `source` or `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => git + } + +If you're defining this for a central/"official" repository, you'll +probably want to make it a "bare" repository. Do this by setting +`ensure` to `bare` instead of `present`: + + vcsrepo { "/path/to/repo": + ensure => bare, + provider => git + } + +To clone/pull a repository +---------------------------- + +To get the current [master] HEAD: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => git, + source => "git://example.com/repo.git" + } + +For a specific revision or branch (can be a commit SHA, tag or branch name): + + vcsrepo { "/path/to/repo": + ensure => present, + provider => git, + source => 'git://example.com/repo.git', + revision => '0c466b8a5a45f6cd7de82c08df2fb4ce1e920a31' + } + + vcsrepo { "/path/to/repo": + ensure => present, + provider => git, + source => 'git://example.com/repo.git', + revision => '1.1.2rc1' + } + + vcsrepo { "/path/to/repo": + ensure => present, + provider => git, + source => 'git://example.com/repo.git', + revision => 'development' + } + +Check out as a user: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => git, + source => 'git://example.com/repo.git', + revision => '0c466b8a5a45f6cd7de82c08df2fb4ce1e920a31', + user => 'someUser' + } + +Keep the repository at the latest revision (note: this will always overwrite local changes to the repository): + + vcsrepo { "/path/to/repo": + ensure => latest, + provider => git, + source => 'git://example.com/repo.git', + revision => 'master', + } + +For sources that use SSH (eg, `username@server:...`) +---------------------------------------------------- + +If your SSH key is associated with a user, simply fill the `user` parameter to use his keys. + +Example: + + user => 'toto' # will use toto's $HOME/.ssh setup + + +Otherwise, manage your SSH keys with Puppet and use `require` in your `vcsrepo` to ensure they are present. +For more information, see the `require` metaparameter documentation[1]. + +More Examples +------------- + +For examples you can run, see `examples/git/` + +[1]: http://docs.puppetlabs.com/references/stable/metaparameter.html#require + diff --git a/modules/vcsrepo/README.HG.markdown b/modules/vcsrepo/README.HG.markdown new file mode 100644 index 0000000..b1680c8 --- /dev/null +++ b/modules/vcsrepo/README.HG.markdown @@ -0,0 +1,55 @@ +Using vcsrepo with Mercurial +============================ + +To create a blank repository +---------------------------- + +Define a `vcsrepo` without a `source` or `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => hg + } + +To clone/pull & update a repository +----------------------------------- + +To get the default branch tip: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => hg, + source => "http://hg.example.com/myrepo" + } + +For a specific changeset, use `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => hg, + source => "http://hg.example.com/myrepo" + revision => '21ea4598c962' + } + +You can also set `revision` to a tag: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => hg, + source => "http://hg.example.com/myrepo" + revision => '1.1.2' + } + +For sources that use SSH (eg, `ssh://...`) +------------------------------------------ + +Manage your SSH keys with Puppet and use `require` in your `vcsrepo` +to ensure they are present. For more information, see the `require` +metaparameter documentation[1]. + +More Examples +------------- + +For examples you can run, see `examples/hg/` + +[1]: http://docs.puppetlabs.com/references/stable/metaparameter.html#require diff --git a/modules/vcsrepo/README.SVN.markdown b/modules/vcsrepo/README.SVN.markdown new file mode 100644 index 0000000..489f5bf --- /dev/null +++ b/modules/vcsrepo/README.SVN.markdown @@ -0,0 +1,47 @@ +Using vcsrepo with Subversion +============================= + +To create a blank repository +---------------------------- + +To create a blank repository suitable for use as a central repository, +define a `vcsrepo` without a `source` or `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => svn + } + +To checkout from a repository +----------------------------- + +Provide a `source` qualified to the branch/tag you want: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => svn, + source => "svn://svnrepo/hello/branches/foo" + } + +You can provide a specific `revision`: + + vcsrepo { "/path/to/repo": + ensure => present, + provider => svn, + source => "svn://svnrepo/hello/branches/foo", + revision => '1234' + } + +For sources that use SSH (eg, `svn+ssh://...`) +---------------------------------------------- + +Manage your SSH keys with Puppet and use `require` in your `vcsrepo` +to ensure they are present. For more information, see the `require` +metaparameter documentation[1]. + +More Examples +------------- + +For examples you can run, see `examples/svn/` + +[1]: http://docs.puppetlabs.com/references/stable/metaparameter.html#require diff --git a/modules/vcsrepo/README.markdown b/modules/vcsrepo/README.markdown new file mode 100644 index 0000000..8487256 --- /dev/null +++ b/modules/vcsrepo/README.markdown @@ -0,0 +1,32 @@ +vcsrepo +======= + +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-vcsrepo.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-vcsrepo) + +Purpose +------- + +This provides a single type, `vcsrepo`. + +This type can be used to describe: + +* A working copy checked out from a (remote or local) source, at an + arbitrary revision +* A "blank" working copy not associated with a source (when it makes + sense for the VCS being used) +* A "blank" central repository (when the distinction makes sense for the VCS + being used) + +Supported Version Control Systems +--------------------------------- + +This module supports a wide range of VCS types, each represented by a +separate provider. + +For information on how to use this module with a specific VCS, see +`README..markdown`. + +License +------- + +See LICENSE. diff --git a/modules/vcsrepo/Rakefile b/modules/vcsrepo/Rakefile new file mode 100644 index 0000000..3e005ff --- /dev/null +++ b/modules/vcsrepo/Rakefile @@ -0,0 +1,13 @@ +require 'spec/rake/spectask' +Spec::Rake::SpecTask.new(:spec) do |spec| + spec.libs << 'lib' << 'spec' + spec.spec_files = FileList['spec/**/*_spec.rb'] +end + +Spec::Rake::SpecTask.new(:rcov) do |spec| + spec.libs << 'lib' << 'spec' + spec.pattern = 'spec/**/*_spec.rb' + spec.rcov = true +end + +task :default => :spec diff --git a/modules/vcsrepo/examples/bzr/branch.pp b/modules/vcsrepo/examples/bzr/branch.pp new file mode 100644 index 0000000..0ed0705 --- /dev/null +++ b/modules/vcsrepo/examples/bzr/branch.pp @@ -0,0 +1,6 @@ +vcsrepo { '/tmp/vcstest-bzr-branch': + ensure => present, + provider => bzr, + source => 'lp:do', + revision => '1312', +} diff --git a/modules/vcsrepo/examples/bzr/init_repo.pp b/modules/vcsrepo/examples/bzr/init_repo.pp new file mode 100644 index 0000000..1129dd7 --- /dev/null +++ b/modules/vcsrepo/examples/bzr/init_repo.pp @@ -0,0 +1,4 @@ +vcsrepo { '/tmp/vcstest-bzr-init': + ensure => present, + provider => bzr, +} diff --git a/modules/vcsrepo/examples/cvs/local.pp b/modules/vcsrepo/examples/cvs/local.pp new file mode 100644 index 0000000..155742e --- /dev/null +++ b/modules/vcsrepo/examples/cvs/local.pp @@ -0,0 +1,11 @@ +vcsrepo { '/tmp/vcstest-cvs-repo': + ensure => present, + provider => cvs, +} + +vcsrepo { '/tmp/vcstest-cvs-workspace-local': + ensure => present, + provider => cvs, + source => '/tmp/vcstest-cvs-repo', + require => Vcsrepo['/tmp/vcstest-cvs-repo'], +} diff --git a/modules/vcsrepo/examples/cvs/remote.pp b/modules/vcsrepo/examples/cvs/remote.pp new file mode 100644 index 0000000..eb9665a --- /dev/null +++ b/modules/vcsrepo/examples/cvs/remote.pp @@ -0,0 +1,5 @@ +vcsrepo { '/tmp/vcstest-cvs-workspace-remote': + ensure => present, + provider => cvs, + source => ':pserver:anonymous@cvs.sv.gnu.org:/sources/leetcvrt', +} diff --git a/modules/vcsrepo/examples/git/bare_init.pp b/modules/vcsrepo/examples/git/bare_init.pp new file mode 100644 index 0000000..4166f6e --- /dev/null +++ b/modules/vcsrepo/examples/git/bare_init.pp @@ -0,0 +1,4 @@ +vcsrepo { '/tmp/vcstest-git-bare': + ensure => bare, + provider => git, +} diff --git a/modules/vcsrepo/examples/git/clone.pp b/modules/vcsrepo/examples/git/clone.pp new file mode 100644 index 0000000..b29a4fd --- /dev/null +++ b/modules/vcsrepo/examples/git/clone.pp @@ -0,0 +1,5 @@ +vcsrepo { '/tmp/vcstest-git-clone': + ensure => present, + provider => git, + source => 'git://github.com/bruce/rtex.git', +} diff --git a/modules/vcsrepo/examples/git/working_copy_init.pp b/modules/vcsrepo/examples/git/working_copy_init.pp new file mode 100644 index 0000000..e3352eb --- /dev/null +++ b/modules/vcsrepo/examples/git/working_copy_init.pp @@ -0,0 +1,4 @@ +vcsrepo { '/tmp/vcstest-git-wc': + ensure => present, + provider => git, +} diff --git a/modules/vcsrepo/examples/hg/clone.pp b/modules/vcsrepo/examples/hg/clone.pp new file mode 100644 index 0000000..be2d955 --- /dev/null +++ b/modules/vcsrepo/examples/hg/clone.pp @@ -0,0 +1,6 @@ +vcsrepo { '/tmp/vcstest-hg-clone': + ensure => present, + provider => hg, + source => 'http://hg.basho.com/riak', + revision => 'riak-0.5.3', +} diff --git a/modules/vcsrepo/examples/hg/init_repo.pp b/modules/vcsrepo/examples/hg/init_repo.pp new file mode 100644 index 0000000..a890804 --- /dev/null +++ b/modules/vcsrepo/examples/hg/init_repo.pp @@ -0,0 +1,4 @@ +vcsrepo { '/tmp/vcstest-hg-init': + ensure => present, + provider => hg, +} diff --git a/modules/vcsrepo/examples/svn/checkout.pp b/modules/vcsrepo/examples/svn/checkout.pp new file mode 100644 index 0000000..f9fc273 --- /dev/null +++ b/modules/vcsrepo/examples/svn/checkout.pp @@ -0,0 +1,5 @@ +vcsrepo { '/tmp/vcstest-svn-checkout': + ensure => present, + provider => svn, + source => 'http://svn.edgewall.org/repos/babel/trunk', +} diff --git a/modules/vcsrepo/examples/svn/server.pp b/modules/vcsrepo/examples/svn/server.pp new file mode 100644 index 0000000..de7c390 --- /dev/null +++ b/modules/vcsrepo/examples/svn/server.pp @@ -0,0 +1,4 @@ +vcsrepo { '/tmp/vcstest-svn-server': + ensure => present, + provider => svn, +} diff --git a/modules/vcsrepo/lib/puppet/provider/vcsrepo.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo.rb new file mode 100644 index 0000000..2c026ba --- /dev/null +++ b/modules/vcsrepo/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/modules/vcsrepo/lib/puppet/provider/vcsrepo/bzr.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo/bzr.rb new file mode 100644 index 0000000..6688ce8 --- /dev/null +++ b/modules/vcsrepo/lib/puppet/provider/vcsrepo/bzr.rb @@ -0,0 +1,85 @@ +require File.join(File.dirname(__FILE__), '..', 'vcsrepo') + +Puppet::Type.type(:vcsrepo).provide(:bzr, :parent => Puppet::Provider::Vcsrepo) do + desc "Supports Bazaar repositories" + + optional_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 + 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) + 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/modules/vcsrepo/lib/puppet/provider/vcsrepo/cvs.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo/cvs.rb new file mode 100644 index 0000000..6dc7882 --- /dev/null +++ b/modules/vcsrepo/lib/puppet/provider/vcsrepo/cvs.rb @@ -0,0 +1,119 @@ +require File.join(File.dirname(__FILE__), '..', 'vcsrepo') + +Puppet::Type.type(:vcsrepo).provide(:cvs, :parent => Puppet::Provider::Vcsrepo) do + desc "Supports CVS repositories/workspaces" + + optional_commands :cvs => 'cvs' + has_features :gzip_compression, :reference_tracking, :modules + + 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? + 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 = (cvs('-nq', 'update', '-d').strip == "") + if (!is_current) then 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 + debug "Checkout is on branch/tag '#{@rev}'" + end + return @rev + end + + def revision=(desired) + at_path do + cvs('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) + cvs(*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) + cvs('-d', path, 'init') + end + + def update_owner + if @resource.value(:owner) or @resource.value(:group) + set_ownership + end + end +end diff --git a/modules/vcsrepo/lib/puppet/provider/vcsrepo/dummy.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo/dummy.rb new file mode 100644 index 0000000..f7b4e54 --- /dev/null +++ b/modules/vcsrepo/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 :vcsrepo => :dummy + + 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/modules/vcsrepo/lib/puppet/provider/vcsrepo/git.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo/git.rb new file mode 100644 index 0000000..76fa315 --- /dev/null +++ b/modules/vcsrepo/lib/puppet/provider/vcsrepo/git.rb @@ -0,0 +1,315 @@ +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 + optional_commands :git => 'git', + :su => 'su' + has_features :bare_repositories, :reference_tracking, :ssh_identity, :multiple_remotes, :user + + 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 + 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("#{@resource.value(:remote)}/HEAD") + elsif branch == '(no branch)' + return get_revision('HEAD') + else + return get_revision("#{@resource.value(:remote)}/%s" % branch) + end + end + + def revision + update_references + current = at_path { git_with_identity('rev-parse', 'HEAD').chomp } + return current unless @resource.value(:revision) + + if tag_revision?(@resource.value(:revision)) + canonical = at_path { git_with_identity('show', @resource.value(:revision)).scan(/^commit (.*)/).to_s } + else + canonical = at_path { git_with_identity('rev-parse', @resource.value(:revision)).chomp } + end + + if current == canonical + @resource.value(:revision) + else + current + end + end + + def revision=(desired) + checkout(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_with_identity('reset', '--hard', "#{@resource.value(:remote)}/#{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_remote_origin_url + current = git_with_identity('config', 'remote.origin.url') + unless @resource.value(:source).nil? + if current.nil? or current.strip != @resource.value(:source) + git_with_identity('config', 'remote.origin.url', @resource.value(:source)) + end + end + end + + def update_references + at_path do + update_remote_origin_url + git_with_identity('fetch', @resource.value(:remote)) + git_with_identity('fetch', '--tags', @resource.value(:remote)) + update_owner_and_excludes + 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) + Dir.chdir("/") do + git_with_identity(*args) + end + 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)) + 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: + # /.git + # to: + # / + 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: + # / + # to: + # /.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_with_identity('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(revision = @resource.value(:revision)) + if !local_branch_revision? && remote_branch_revision? + at_path { git_with_identity('checkout', '-b', revision, '--track', "#{@resource.value(:remote)}/#{revision}") } + else + at_path { git_with_identity('checkout', '--force', revision) } + end + end + + def reset(desired) + at_path do + git_with_identity('reset', '--hard', desired) + end + end + + def update_submodules + at_path do + git_with_identity('submodule', 'init') + git_with_identity('submodule', 'update') + git_with_identity('submodule', 'foreach', 'git', 'submodule', 'init') + git_with_identity('submodule', 'foreach', 'git', 'submodule', 'update') + end + end + + 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}/ } + if branch.length > 0 + return branch + end + 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_with_identity('branch', '-a') }.gsub('*', ' ').split(/\n/).map { |line| line.strip } + end + + def on_branch? + at_path { git_with_identity('branch', '-a') }.split(/\n/).grep(/\*/).first.to_s.gsub('*', '').strip + end + + def tags + at_path { git_with_identity('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 + update_remote_origin_url + git_with_identity('fetch', @resource.value(:remote)) + git_with_identity('fetch', '--tags', @resource.value(:remote)) + end + current = at_path { git_with_identity('rev-parse', rev).strip } + if @resource.value(:revision) + if 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 } + end + current = @resource.value(:revision) if current == canonical + end + update_owner_and_excludes + 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 + + def git_with_identity(*args) + if @resource.value(:identity) + Tempfile.open('git-helper') do |f| + f.puts '#!/bin/sh' + 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) + su(@resource.value(:user), '-c', "git #{args.join(' ')}" ) + else + git(*args) + end + end +end diff --git a/modules/vcsrepo/lib/puppet/provider/vcsrepo/hg.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo/hg.rb new file mode 100644 index 0000000..67a2a82 --- /dev/null +++ b/modules/vcsrepo/lib/puppet/provider/vcsrepo/hg.rb @@ -0,0 +1,103 @@ +require File.join(File.dirname(__FILE__), '..', 'vcsrepo') + +Puppet::Type.type(:vcsrepo).provide(:hg, :parent => Puppet::Provider::Vcsrepo) do + desc "Supports Mercurial repositories" + + optional_commands :hg => 'hg' + 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 + begin + hg('pull') + rescue + end + 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/modules/vcsrepo/lib/puppet/provider/vcsrepo/svn.rb b/modules/vcsrepo/lib/puppet/provider/vcsrepo/svn.rb new file mode 100644 index 0000000..c6603b0 --- /dev/null +++ b/modules/vcsrepo/lib/puppet/provider/vcsrepo/svn.rb @@ -0,0 +1,106 @@ +require File.join(File.dirname(__FILE__), '..', 'vcsrepo') + +Puppet::Type.type(:vcsrepo).provide(:svn, :parent => Puppet::Provider::Vcsrepo) do + desc "Supports Subversion repositories" + + optional_commands :svn => 'svn', + :svnadmin => 'svnadmin' + + has_features :filesystem_types, :reference_tracking, :basic_auth + + def create + if !@resource.value(:source) + create_repository(@resource.value(:path)) + else + checkout_repository(@resource.value(:source), + @resource.value(:path), + @resource.value(:revision)) + end + update_owner + 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 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 + + return args + end + + def latest + args = buildargs.push('info', '-r', 'HEAD') + at_path do + svn(*args)[/^Last Changed Rev:\s+(\d+)/m, 1] + end + end + + def revision + args = buildargs.push('info') + at_path do + svn(*args)[/^Last Changed Rev:\s+(\d+)/m, 1] + end + end + + def revision=(desired) + args = buildargs.push('update', '-r', desired) + at_path do + svn(*args) + end + update_owner + end + + private + + def checkout_repository(source, path, revision) + args = buildargs.push('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 + + def update_owner + if @resource.value(:owner) or @resource.value(:group) + set_ownership + end + end +end diff --git a/modules/vcsrepo/lib/puppet/type/vcsrepo.rb b/modules/vcsrepo/lib/puppet/type/vcsrepo.rb new file mode 100644 index 0000000..45ac455 --- /dev/null +++ b/modules/vcsrepo/lib/puppet/type/vcsrepo.rb @@ -0,0 +1,180 @@ +require 'pathname' + +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" + + 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 + return false + end + when :bare + return is == :bare + end + end + + newvalue :present do + notice "Creating repository from present" + provider.create + end + + newvalue :bare, :required_features => [:bare_repositories] do + if !provider.exists? + provider.create + end + 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 + notice "Creating repository from latest" + provider.create + end + end + + def retrieve + prov = @resource.provider + if prov + if prov.working_copy_exists? + (@should.include?(:latest) && 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 :user do + desc "The user to run for repository operations" + 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 + + 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 + + newparam :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 + +end diff --git a/modules/vcsrepo/spec/fixtures/bzr_version_info.txt b/modules/vcsrepo/spec/fixtures/bzr_version_info.txt new file mode 100644 index 0000000..88a56a1 --- /dev/null +++ b/modules/vcsrepo/spec/fixtures/bzr_version_info.txt @@ -0,0 +1,5 @@ +revision-id: menesis@pov.lt-20100309191856-4wmfqzc803fj300x +date: 2010-03-09 21:18:56 +0200 +build-date: 2010-03-14 00:42:43 -0800 +revno: 2634 +branch-nick: mytest diff --git a/modules/vcsrepo/spec/fixtures/git_branch_a.txt b/modules/vcsrepo/spec/fixtures/git_branch_a.txt new file mode 100644 index 0000000..2c99829 --- /dev/null +++ b/modules/vcsrepo/spec/fixtures/git_branch_a.txt @@ -0,0 +1,14 @@ + feature/foo + feature/bar + feature/baz + feature/quux + only/local +* master + refactor/foo + origin/HEAD + origin/feature/foo + origin/feature/bar + origin/feature/baz + origin/feature/quux + origin/only/remote + origin/master diff --git a/modules/vcsrepo/spec/fixtures/hg_parents.txt b/modules/vcsrepo/spec/fixtures/hg_parents.txt new file mode 100644 index 0000000..46173df --- /dev/null +++ b/modules/vcsrepo/spec/fixtures/hg_parents.txt @@ -0,0 +1,6 @@ +changeset: 3:34e6012c783a +parent: 2:21ea4598c962 +parent: 1:9d0ff0028458 +user: Test User +date: Fri Aug 07 13:13:02 2009 -0400 +summary: merge diff --git a/modules/vcsrepo/spec/fixtures/hg_tags.txt b/modules/vcsrepo/spec/fixtures/hg_tags.txt new file mode 100644 index 0000000..53792e5 --- /dev/null +++ b/modules/vcsrepo/spec/fixtures/hg_tags.txt @@ -0,0 +1,18 @@ +tip 1019:bca3f20b249b +0.9.1 1017:76ce7cca95d8 +0.9 1001:dbaa6f4ec585 +0.8 839:65b66ac0fc83 +0.7.1 702:e1357f00129f +0.7 561:7b2af3b4c968 +0.6.3 486:e38077f4e4aa +0.6.2 405:07bb099b7b10 +0.6.1 389:93750f3fbbe2 +0.6 369:34e6012c783a +0.5.3 321:5ffa6ae7e699 +0.5.2 318:fdc2c2e4cebe +0.5.1 315:33a5ea0cbe7a +0.5 313:47490716f4c9 +0.4 240:47fa3a14cc63 +0.3.1 132:bc231db18e1c +0.3 130:661615e510dd +0.2 81:f98d13b442f6 diff --git a/modules/vcsrepo/spec/fixtures/svn_info.txt b/modules/vcsrepo/spec/fixtures/svn_info.txt new file mode 100644 index 0000000..d2a975b --- /dev/null +++ b/modules/vcsrepo/spec/fixtures/svn_info.txt @@ -0,0 +1,10 @@ +Path: . +URL: http://example.com/svn/trunk +Repository Root: http://example.com/svn +Repository UUID: 75246ace-e253-0410-96dd-a7613ca8dc81 +Revision: 4 +Node Kind: directory +Schedule: normal +Last Changed Author: jon +Last Changed Rev: 3 +Last Changed Date: 2008-08-07 11:34:25 -0700 (Thu, 07 Aug 2008) diff --git a/modules/vcsrepo/spec/spec.opts b/modules/vcsrepo/spec/spec.opts new file mode 100644 index 0000000..91cd642 --- /dev/null +++ b/modules/vcsrepo/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/modules/vcsrepo/spec/spec_helper.rb b/modules/vcsrepo/spec/spec_helper.rb new file mode 100644 index 0000000..3bac650 --- /dev/null +++ b/modules/vcsrepo/spec/spec_helper.rb @@ -0,0 +1,25 @@ +require 'pathname' +dir = Pathname.new(__FILE__).parent +$LOAD_PATH.unshift(dir, dir + 'lib', dir + '../lib') + +require 'test/unit' +require 'mocha' +require 'puppet' +gem 'rspec', '>= 1.2.9' +require 'spec/autorun' + +Dir[File.join(File.dirname(__FILE__), 'support', '*.rb')].each do |support_file| + require support_file +end + +Spec::Runner.configure do |config| + config.mock_with :mocha + config.include(FixtureHelpers) + config.include(FilesystemHelpers) +end + +# We need this because the RAL uses 'should' as a method. This +# allows us the same behaviour but with a different method name. +class Object + alias :must :should +end diff --git a/modules/vcsrepo/spec/support/filesystem_helpers.rb b/modules/vcsrepo/spec/support/filesystem_helpers.rb new file mode 100644 index 0000000..15e2ca7 --- /dev/null +++ b/modules/vcsrepo/spec/support/filesystem_helpers.rb @@ -0,0 +1,18 @@ +module FilesystemHelpers + + def expects_chdir(path = resource.value(:path)) + Dir.expects(:chdir).with(path).at_least_once.yields + end + + def expects_mkdir(path = resource.value(:path)) + Dir.expects(:mkdir).with(path).at_least_once + end + + def expects_rm_rf(path = resource.value(:path)) + FileUtils.expects(:rm_rf).with(path) + end + + def expects_directory?(returns = true, path = resource.value(:path)) + File.expects(:directory?).with(path).returns(returns) + end +end diff --git a/modules/vcsrepo/spec/support/fixture_helpers.rb b/modules/vcsrepo/spec/support/fixture_helpers.rb new file mode 100644 index 0000000..8a0e0a0 --- /dev/null +++ b/modules/vcsrepo/spec/support/fixture_helpers.rb @@ -0,0 +1,7 @@ +module FixtureHelpers + + def fixture(name, ext = '.txt') + File.read(File.join(File.dirname(__FILE__), '..', 'fixtures', name.to_s + ext)) + end + +end diff --git a/modules/vcsrepo/spec/support/provider_example_group.rb b/modules/vcsrepo/spec/support/provider_example_group.rb new file mode 100644 index 0000000..1431f78 --- /dev/null +++ b/modules/vcsrepo/spec/support/provider_example_group.rb @@ -0,0 +1,80 @@ +class ProviderExampleGroup < Spec::Example::ExampleGroup + + # Allow access to the current resource + attr_reader :resource + + # Build up the values for the resource in this scope + before :each do + resource_hash = example_group_hierarchy.inject({}) do |memo, klass| + memo.merge(klass.options[:resource] || {}) + end + full_hash = resource_hash.merge(:provider => described_class.name) + @resource = described_class.resource_type.new(full_hash) + end + + # Build the provider + subject { described_class.new(@resource) } + + # Allow access to it via +provider+ + alias :provider :subject + + # Generate a context for a provider operating on a resource with: + # + # call-seq: + # + # # A parameter/property set (when the value isn't important) + # resource_with :source do + # # ... + # end + # + # # A parameter/property set to a specific value + # resource_with :source => 'a-specific-value' do + # # ... + # end + # + # Note: Choose one or the other (mixing will create two separate contexts) + # + def self.resource_with(*params, &block) + params_with_values = params.last.is_a?(Hash) ? params.pop : {} + build_value_context(params_with_values, &block) + build_existence_context(*params, &block) + end + + def self.build_existence_context(*params, &block) #:nodoc: + unless params.empty? + text = params.join(', ') + placeholders = params.inject({}) { |memo, key| memo.merge(key => 'an-unimportant-value') } + context("and with a #{text}", {:resource => placeholders}, &block) + end + end + + def self.build_value_context(params = {}, &block) #:nodoc: + unless params.empty? + text = params.map { |k, v| "#{k} => #{v.inspect}" }.join(' and with ') + context("and with #{text}", {:resource => params}, &block) + end + end + + + # Generate a context for a provider operating on a resource without + # a given parameter/property. + # + # call-seq: + # + # resource_without :source do + # # ... + # end + # + def self.resource_without(field, &block) + context("and without a #{field}", &block) + end + +end + +Spec::Example::ExampleGroupFactory.register(:provider, ProviderExampleGroup) + +# Outside wrapper to lookup a provider and start the spec using ProviderExampleGroup +def describe_provider(type_name, provider_name, options = {}, &block) + provider_class = Puppet::Type.type(type_name).provider(provider_name) + describe(provider_class, options.merge(:type => :provider), &block) +end diff --git a/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/bzr_spec.rb b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/bzr_spec.rb new file mode 100644 index 0000000..c0231e9 --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/bzr_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe_provider :vcsrepo, :bzr, :resource => {:path => '/tmp/vcsrepo'} do + + describe 'creating' do + resource_with :source do + resource_with :revision do + it "should execute 'bzr clone -r' with the revision" do + provider.expects(:bzr).with('branch', '-r', resource.value(:revision), resource.value(:source), resource.value(:path)) + provider.create + end + end + resource_without :revision do + it "should just execute 'bzr clone' without a revision" do + provider.expects(:bzr).with('branch', resource.value(:source), resource.value(:path)) + provider.create + end + end + end + resource_without :source do + it "should execute 'bzr init'" do + provider.expects(:bzr).with('init', resource.value(:path)) + provider.create + end + end + end + + describe 'destroying' do + it "it should remove the directory" do + expects_rm_rf + provider.destroy + end + end + + describe "checking existence" do + it "should check for the directory" do + expects_directory?(true, File.join(resource.value(:path), '.bzr')) + provider.exists? + end + end + + describe "checking the revision property" do + before do + expects_chdir + provider.expects(:bzr).with('version-info').returns(fixture(:bzr_version_info)) + @current_revid = 'menesis@pov.lt-20100309191856-4wmfqzc803fj300x' + end + context "when given a non-revid as the resource revision", :resource => {:revision => '2634'} do + context "when its revid is not different than the current revid" do + before do + provider.expects(:bzr).with('revision-info', resource.value(:revision)).returns("#{resource.value(:revision)} menesis@pov.lt-20100309191856-4wmfqzc803fj300x\n") + end + it "should return the ref" do + provider.revision.should == resource.value(:revision) + end + end + context "when its revid is different than the current revid", :resource => {:revision => '2636'} do + it "should return the current revid" do + provider.expects(:bzr).with('revision-info', resource.value(:revision)).returns("2635 foo\n") + provider.revision.should == @current_revid + end + end + end + context "when given a revid as the resource revision" do + context "when it is the same as the current revid", :resource => {:revision => 'menesis@pov.lt-20100309191856-4wmfqzc803fj300x'} do + before do + provider.expects(:bzr).with('revision-info', resource.value(:revision)).returns("1234 #{resource.value(:revision)}\n") + end + it "should return it" do + provider.revision.should == resource.value(:revision) + end + end + context "when it is not the same as the current revid", :resource => {:revision => 'menesis@pov.lt-20100309191856-4wmfqzc803fj300y'} do + it "should return the current revid" do + provider.expects(:bzr).with('revision-info', resource.value(:revision)).returns("2636 foo\n") + provider.revision.should == @current_revid + end + end + end + end + + describe "setting the revision property" do + it "should use 'bzr update -r' with the revision" do + expects_chdir + provider.expects(:bzr).with('update', '-r', 'somerev') + provider.revision = 'somerev' + end + end + +end diff --git a/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/cvs_spec.rb b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/cvs_spec.rb new file mode 100644 index 0000000..aad54cc --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/cvs_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +describe_provider :vcsrepo, :cvs, :resource => {:path => '/tmp/vcsrepo'} do + + describe 'creating' do + context "with a source", :resource => {:source => ':ext:source@example.com:/foo/bar'} do + resource_with :revision do + it "should execute 'cvs checkout' and 'cvs update -r'" do + provider.expects(:cvs).with('-d', resource.value(:source), 'checkout', '-r', 'an-unimportant-value', '-d', 'vcsrepo', 'bar') + expects_chdir(File.dirname(resource.value(:path))) + #provider.expects(:cvs).with('update', '-r', resource.value(:revision), '.') + provider.create + end + end + + resource_without :revision do + it "should just execute 'cvs checkout' without a revision" do + provider.expects(:cvs).with('-d', resource.value(:source), 'checkout', '-d', File.basename(resource.value(:path)), File.basename(resource.value(:source))) + provider.create + end + end + + context "with a compression", :resource => {:compression => '3'} do + it "should just execute 'cvs checkout' without a revision" do + provider.expects(:cvs).with('-d', resource.value(:source), '-z', '3', 'checkout', '-d', File.basename(resource.value(:path)), File.basename(resource.value(:source))) + provider.create + end + end + end + + context "when a source is not given" do + it "should execute 'cvs init'" do + provider.expects(:cvs).with('-d', resource.value(:path), 'init') + provider.create + end + end + end + + describe 'destroying' do + it "it should remove the directory" do + expects_rm_rf + provider.destroy + end + end + + describe "checking existence" do + resource_with :source do + it "should check for the CVS directory" do + File.expects(:directory?).with(File.join(resource.value(:path), 'CVS')) + provider.exists? + end + end + + resource_without :source do + it "should check for the CVSROOT directory" do + File.expects(:directory?).with(File.join(resource.value(:path), 'CVSROOT')) + provider.exists? + end + end + end + + describe "checking the revision property" do + before do + @tag_file = File.join(resource.value(:path), 'CVS', 'Tag') + end + + context "when CVS/Tag exists" do + before do + @tag = 'TAG' + File.expects(:exist?).with(@tag_file).returns(true) + end + it "should read CVS/Tag" do + File.expects(:read).with(@tag_file).returns("T#{@tag}") + provider.revision.should == @tag + end + end + + context "when CVS/Tag does not exist" do + before do + File.expects(:exist?).with(@tag_file).returns(false) + end + it "assumes HEAD" do + provider.revision.should == 'HEAD' + end + end + end + + describe "when setting the revision property" do + before do + @tag = 'SOMETAG' + end + + it "should use 'cvs update -dr'" do + expects_chdir + provider.expects(:cvs).with('update', '-dr', @tag, '.') + provider.revision = @tag + end + end + +end diff --git a/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/dummy_spec.rb b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/dummy_spec.rb new file mode 100644 index 0000000..a945888 --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/dummy_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe_provider :vcsrepo, :dummy, :resource => {:path => '/tmp/vcsrepo'} do + + context 'dummy' do + resource_with :source do + resource_with :ensure => :present do + context "with nothing doing", :resource => {:revision => 'foo'} do + it "should raise an exception" do + proc { provider.working_copy_exists? }.should raise_error(RuntimeError) + end + end + end + end + end + +end diff --git a/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/git_spec.rb b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/git_spec.rb new file mode 100644 index 0000000..cb6c0c6 --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/git_spec.rb @@ -0,0 +1,248 @@ +require 'spec_helper' + +describe_provider :vcsrepo, :git, :resource => {:path => '/tmp/vcsrepo'} do + + context 'creating' do + resource_with :source do + resource_with :ensure => :present do + context "with a revision that is a remote branch", :resource => {:revision => 'only/remote'} do + it "should execute 'git clone' and 'git checkout -b'" do + provider.expects(:git).with('clone', resource.value(:source), resource.value(:path)) + expects_chdir('/') + expects_chdir + provider.expects(:update_submodules) + provider.expects(:git).with('branch', '-a').returns(resource.value(:revision)) + provider.expects(:git).with('checkout', '--force', resource.value(:revision)) + provider.create + end + end + context "with a revision that is not a remote branch", :resource => {:revision => 'a-commit-or-tag'} do + it "should execute 'git clone' and 'git reset --hard'" do + provider.expects(:git).with('clone', resource.value(:source), resource.value(:path)) + expects_chdir('/') + expects_chdir + provider.expects(:update_submodules) + provider.expects(:git).with('branch', '-a').returns(resource.value(:revision)) + provider.expects(:git).with('checkout', '--force', resource.value(:revision)) + provider.create + end + end + resource_without :revision do + it "should execute 'git clone' and submodule commands" do + provider.expects(:git).with('clone', resource.value(:source), resource.value(:path)) + provider.expects(:update_submodules) + provider.create + end + end + end + + resource_with :ensure => :bare do + resource_with :revision do + it "should just execute 'git clone --bare'" do + provider.expects(:git).with('clone', '--bare', resource.value(:source), resource.value(:path)) + provider.create + end + end + + resource_without :revision do + it "should just execute 'git clone --bare'" do + provider.expects(:git).with('clone', '--bare', resource.value(:source), resource.value(:path)) + provider.create + end + end + end + end + + context "when a source is not given" do + resource_with :ensure => :present do + context "when the path does not exist" do + it "should execute 'git init'" do + expects_mkdir + expects_chdir + expects_directory?(false) + provider.expects(:bare_exists?).returns(false) + provider.expects(:git).with('init') + provider.create + end + end + + context "when the path is a bare repository" do + it "should convert it to a working copy" do + provider.expects(:bare_exists?).returns(true) + provider.expects(:convert_bare_to_working_copy) + provider.create + end + end + + context "when the path is not a repository" do + it "should raise an exception" do + provider.expects(:path_exists?).returns(true) + proc { provider.create }.should raise_error(Puppet::Error) + end + end + end + + resource_with :ensure => :bare do + context "when the path does not exist" do + it "should execute 'git init --bare'" do + expects_chdir + expects_mkdir + expects_directory?(false) + provider.expects(:working_copy_exists?).returns(false) + provider.expects(:git).with('init', '--bare') + provider.create + end + end + + context "when the path is a working copy repository" do + it "should convert it to a bare repository" do + provider.expects(:working_copy_exists?).returns(true) + provider.expects(:convert_working_copy_to_bare) + provider.create + end + end + + context "when the path is not a repository" do + it "should raise an exception" do + expects_directory?(true) + proc { provider.create }.should raise_error(Puppet::Error) + end + end + end + end + + end + + context 'destroying' do + it "it should remove the directory" do + expects_rm_rf + provider.destroy + end + end + + context "checking the revision property" do + resource_with :revision do + before do + expects_chdir + provider.expects(:git).with('rev-parse', 'HEAD').returns('currentsha') + end + + context "when its SHA is not different than the current SHA" do + it "should return the ref" do + provider.expects(:git).with('config', 'remote.origin.url').returns('') + provider.expects(:git).with('fetch', 'origin') # FIXME + provider.expects(:git).with('fetch', '--tags', 'origin') + provider.expects(:git).with('rev-parse', resource.value(:revision)).returns('currentsha') + provider.expects(:git).with('tag', '-l').returns("Hello") + provider.revision.should == resource.value(:revision) + end + end + + context "when its SHA is different than the current SHA" do + it "should return the current SHA" do + provider.expects(:git).with('config', 'remote.origin.url').returns('') + provider.expects(:git).with('fetch', 'origin') # FIXME + provider.expects(:git).with('fetch', '--tags', 'origin') + provider.expects(:git).with('rev-parse', resource.value(:revision)).returns('othersha') + provider.expects(:git).with('tag', '-l').returns("Hello") + provider.revision.should == 'currentsha' + end + end + + context "when the source is modified" do + resource_with :source => 'git://git@foo.com/bar.git' do + it "should update the origin url" do + provider.expects(:git).with('config', 'remote.origin.url').returns('old') + provider.expects(:git).with('config', 'remote.origin.url', 'git://git@foo.com/bar.git') + provider.expects(:git).with('fetch', 'origin') # FIXME + provider.expects(:git).with('fetch', '--tags', 'origin') + provider.expects(:git).with('rev-parse', resource.value(:revision)).returns('currentsha') + provider.expects(:git).with('tag', '-l').returns("Hello") + provider.revision.should == resource.value(:revision) + end + end + end + end + end + + context "setting the revision property" do + before do + expects_chdir + end + context "when it's an existing local branch", :resource => {:revision => 'feature/foo'} do + it "should use 'git fetch' and 'git reset'" do + provider.expects(:update_submodules) + provider.expects(:git).with('branch', '-a').returns(resource.value(:revision)) + provider.expects(:git).with('checkout', '--force', resource.value(:revision)) + provider.expects(:git).with('branch', '-a').returns(resource.value(:revision)) + provider.expects(:git).with('reset', '--hard', "origin/#{resource.value(:revision)}") + provider.revision = resource.value(:revision) + end + end + context "when it's a remote branch", :resource => {:revision => 'only/remote'} do + it "should use 'git fetch' and 'git reset'" do + provider.expects(:update_submodules) + provider.expects(:git).with('branch', '-a').returns(resource.value(:revision)) + provider.expects(:git).with('checkout', '--force', resource.value(:revision)) + provider.expects(:git).with('branch', '-a').returns(resource.value(:revision)) + provider.expects(:git).with('reset', '--hard', "origin/#{resource.value(:revision)}") + provider.revision = resource.value(:revision) + end + end + context "when it's a commit or tag", :resource => {:revision => 'a-commit-or-tag'} do + it "should use 'git fetch' and 'git reset'" do + provider.expects(:git).with('branch', '-a').returns(fixture(:git_branch_a)) + provider.expects(:git).with('checkout', '--force', resource.value(:revision)) + provider.expects(:git).with('branch', '-a').returns(fixture(:git_branch_a)) + provider.expects(:git).with('submodule', 'init') + provider.expects(:git).with('submodule', 'update') + provider.expects(:git).with('branch', '-a').returns(fixture(:git_branch_a)) + provider.expects(:git).with('submodule', 'foreach', 'git', 'submodule', 'init') + provider.expects(:git).with('submodule', 'foreach', 'git', 'submodule', 'update') + provider.revision = resource.value(:revision) + end + end + end + + context "updating references" do + it "should use 'git fetch --tags'" do + expects_chdir + provider.expects(:git).with('config', 'remote.origin.url').returns('') + provider.expects(:git).with('fetch', 'origin') + provider.expects(:git).with('fetch', '--tags', 'origin') + provider.update_references + end + end + + context "checking if revision" do + before do + expects_chdir + provider.expects(:git).with('branch', '-a').returns(fixture(:git_branch_a)) + end + context "is a local branch" do + context "when it's listed in 'git branch -a'", :resource => {:revision => 'feature/foo'} do + it "should return true" do + provider.should be_local_branch_revision + end + end + context "when it's not listed in 'git branch -a'" , :resource => {:revision => 'feature/notexist'}do + it "should return false" do + provider.should_not be_local_branch_revision + end + end + end + context "is a remote branch" do + context "when it's listed in 'git branch -a' with an 'origin/' prefix", :resource => {:revision => 'only/remote'} do + it "should return true" do + provider.should be_remote_branch_revision + end + end + context "when it's not listed in 'git branch -a' with an 'origin/' prefix" , :resource => {:revision => 'only/local'}do + it "should return false" do + provider.should_not be_remote_branch_revision + end + end + end + end + +end diff --git a/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/hg_spec.rb b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/hg_spec.rb new file mode 100644 index 0000000..f17aa2f --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/hg_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe_provider :vcsrepo, :hg, :resource => {:path => '/tmp/vcsrepo'} do + + describe 'creating' do + resource_with :source do + resource_with :revision do + it "should execute 'hg clone -u' with the revision" do + provider.expects(:hg).with('clone', '-u', + resource.value(:revision), + resource.value(:source), + resource.value(:path)) + provider.create + end + end + + resource_without :revision do + it "should just execute 'hg clone' without a revision" do + provider.expects(:hg).with('clone', resource.value(:source), resource.value(:path)) + provider.create + end + end + end + + context "when a source is not given" do + it "should execute 'hg init'" do + provider.expects(:hg).with('init', resource.value(:path)) + provider.create + end + end + end + + describe 'destroying' do + it "it should remove the directory" do + expects_rm_rf + provider.destroy + end + end + + describe "checking existence" do + it "should check for the directory" do + expects_directory?(true, File.join(resource.value(:path), '.hg')) + provider.exists? + end + end + + describe "checking the revision property" do + before do + expects_chdir + end + + context "when given a non-SHA as the resource revision" do + before do + provider.expects(:hg).with('parents').returns(fixture(:hg_parents)) + provider.expects(:hg).with('tags').returns(fixture(:hg_tags)) + end + + context "when its SHA is not different than the current SHA", :resource => {:revision => '0.6'} do + it "should return the ref" do + provider.revision.should == '0.6' + end + end + + context "when its SHA is different than the current SHA", :resource => {:revision => '0.5.3'} do + it "should return the current SHA" do + provider.revision.should == '34e6012c783a' + end + end + end + context "when given a SHA as the resource revision" do + before do + provider.expects(:hg).with('parents').returns(fixture(:hg_parents)) + end + + context "when it is the same as the current SHA", :resource => {:revision => '34e6012c783a'} do + it "should return it" do + provider.expects(:hg).with('tags').returns(fixture(:hg_tags)) + provider.revision.should == resource.value(:revision) + end + end + + context "when it is not the same as the current SHA", :resource => {:revision => 'not-the-same'} do + it "should return the current SHA" do + provider.expects(:hg).with('tags').returns(fixture(:hg_tags)) + provider.revision.should == '34e6012c783a' + end + end + end + end + + describe "setting the revision property" do + before do + @revision = '6aa99e9b3ab1' + end + it "should use 'hg update ---clean -r'" do + expects_chdir + provider.expects(:hg).with('pull') + provider.expects(:hg).with('merge') + provider.expects(:hg).with('update', '--clean', '-r', @revision) + provider.revision = @revision + end + end + +end diff --git a/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/svn_spec.rb b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/svn_spec.rb new file mode 100644 index 0000000..75d58f9 --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/provider/vcsrepo/svn_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe_provider :vcsrepo, :svn, :resource => {:path => '/tmp/vcsrepo'} do + + describe 'creating' do + resource_with :source do + resource_with :revision do + it "should execute 'svn checkout' with a revision" do + provider.expects(:svn).with('--non-interactive', 'checkout', '-r', + resource.value(:revision), + resource.value(:source), + resource.value(:path)) + provider.create + end + end + resource_without :revision do + it "should just execute 'svn checkout' without a revision" do + provider.expects(:svn).with('--non-interactive', 'checkout', + resource.value(:source), + resource.value(:path)) + provider.create + end + end + end + resource_without :source do + resource_with :fstype do + it "should execute 'svnadmin create' with an '--fs-type' option" do + provider.expects(:svnadmin).with('create', '--fs-type', + resource.value(:fstype), + resource.value(:path)) + provider.create + end + end + resource_without :fstype do + it "should execute 'svnadmin create' without an '--fs-type' option" do + provider.expects(:svnadmin).with('create', resource.value(:path)) + provider.create + end + end + end + end + + describe 'destroying' do + it "it should remove the directory" do + expects_rm_rf + provider.destroy + end + end + + describe "checking existence" do + it "should check for the directory" do + expects_directory?(true, File.join(resource.value(:path), '.svn')) + provider.exists? + end + end + + describe "checking the revision property" do + before do + provider.expects(:svn).with('--non-interactive', 'info').returns(fixture(:svn_info)) + end + it "should use 'svn info'" do + expects_chdir + provider.revision.should == '3' # From 'Last Changed Rev', not 'Revision' + end + end + + describe "setting the revision property" do + before do + @revision = '30' + end + it "should use 'svn update'" do + expects_chdir + provider.expects(:svn).with('--non-interactive', 'update', '-r', @revision) + provider.revision = @revision + end + end + +end diff --git a/modules/vcsrepo/spec/unit/puppet/type/README.markdown b/modules/vcsrepo/spec/unit/puppet/type/README.markdown new file mode 100644 index 0000000..1ee19ac --- /dev/null +++ b/modules/vcsrepo/spec/unit/puppet/type/README.markdown @@ -0,0 +1,4 @@ +Resource Type Specs +=================== + +Define specs for your resource types in this directory.