diff --git a/lib/puppet/provider/vcsrepo/svn.rb b/lib/puppet/provider/vcsrepo/svn.rb index 69b73b9..c0d985e 100644 --- a/lib/puppet/provider/vcsrepo/svn.rb +++ b/lib/puppet/provider/vcsrepo/svn.rb @@ -1,312 +1,312 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:svn, parent: Puppet::Provider::Vcsrepo) do desc 'Supports Subversion repositories' commands svn: 'svn', svnadmin: 'svnadmin', svnlook: 'svnlook' has_features :filesystem_types, :reference_tracking, :basic_auth, :configuration, :conflict, :depth, :include_paths def create check_force if !@resource.value(:source) if @resource.value(:includes) raise Puppet::Error, 'Specifying include paths on a nonexistent repo.' end create_repository(@resource.value(:path)) else if @resource.value(:basic_auth_username) && !@resource.value(:basic_auth_password) raise("You must specify the HTTP basic authentication password for user '#{@resource.value(:basic_auth_username)}'") end if !@resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) raise('You must specify the HTTP basic authentication username') end if @resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) if %r{[\u007B-\u00BF\u02B0-\u037F\u2000-\u2BFF]}.match?(@resource.value(:basic_auth_password).to_s) raise('The password can not contain non-ASCII characters') end end checkout_repository(@resource.value(:source), @resource.value(:path), @resource.value(:revision), @resource.value(:depth)) end if @resource.value(:includes) validate_version update_includes(@resource.value(:includes)) end update_owner end def working_copy_exists? return false unless File.directory?(@resource.value(:path)) if @resource.value(:source) begin svn_wrapper('info', @resource.value(:path)) true rescue Puppet::ExecutionFailure => detail if %r{This client is too old}.match?(detail.message) raise Puppet::Error, detail.message end false end else begin svnlook('uuid', @resource.value(:path)) true rescue Puppet::ExecutionFailure false end end end def exists? working_copy_exists? end def destroy FileUtils.rm_rf(@resource.value(:path)) end def latest? at_path do (revision >= latest) && (@resource.value(:source) == source) end end def buildargs args = ['--non-interactive'] if @resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) args.push('--username', @resource.value(:basic_auth_username)) - args.push('--password', sensitive? ? @resource.value(:basic_auth_password).unwrap : @resource.value(:basic_auth_password)) + args.push('--password', @resource.value(:basic_auth_password)) args.push('--no-auth-cache') end if @resource.value(:configuration) args.push('--config-dir', @resource.value(:configuration)) end if @resource.value(:trust_server_cert) != :false args.push('--trust-server-cert') end args end def latest args = buildargs.push('info', '-r', 'HEAD') at_path do svn_wrapper(*args)[%r{^Revision:\s+(\d+)}m, 1] end end def source args = buildargs.push('info') at_path do svn_wrapper(*args)[%r{^URL:\s+(\S+)}m, 1] end end def source=(desired) args = buildargs.push('switch') if @resource.value(:force) args.push('--force') end if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end if @resource.value(:conflict) args.push('--accept', @resource.value(:conflict)) end args.push(desired) at_path do svn_wrapper(*args) end update_owner end def revision args = buildargs.push('info') at_path do svn_wrapper(*args)[%r{^Revision:\s+(\d+)}m, 1] end end def revision=(desired) args = if @resource.value(:source) buildargs.push('switch', '-r', desired, @resource.value(:source)) else buildargs.push('update', '-r', desired) end if @resource.value(:force) args.push('--force') end if @resource.value(:conflict) args.push('--accept', @resource.value(:conflict)) end at_path do svn_wrapper(*args) end update_owner end def includes return nil if Gem::Version.new(return_svn_client_version) < Gem::Version.new('1.6.0') get_includes('.') end def includes=(desired) validate_version exists = includes old_paths = exists - desired new_paths = desired - exists # Remove paths that are no longer specified old_paths.each { |path| delete_include(path) } update_includes(new_paths) end private def svn_wrapper(*args) Puppet::Util::Execution.execute("svn #{args.join(' ')}", sensitive: sensitive?) end def sensitive? (@resource.parameters.key?(:basic_auth_password) && @resource.parameters[:basic_auth_password].sensitive) ? true : false # Check if there is a sensitive parameter end SKIP_DIRS = ['.', '..', '.svn'].freeze def get_includes(directory) at_path do args = buildargs.push('info', directory) if svn_wrapper(*args)[%r{^Depth:\s+(\w+)}m, 1] != 'empty' return directory[2..-1].gsub(File::SEPARATOR, '/') end Dir.entries(directory).map { |entry| next if SKIP_DIRS.include?(entry) entry = File.join(directory, entry) if File.directory?(entry) get_includes(entry) elsif File.file?(entry) entry[2..-1].gsub(File::SEPARATOR, '/') end }.flatten.compact! end end def delete_include(path) at_path do # svn version 1.6 has an incorrect implementation of the `exclude` # parameter to `--set-depth`; it doesn't handle files, only # directories. I know, I rolled my eyes, too. svn_ver = return_svn_client_version if Gem::Version.new(svn_ver) < Gem::Version.new('1.7.0') && !File.directory?(path) # In the non-happy case, we delete the file, and check if the only # thing left in that directory is the .svn folder. If that's the case, # the loop below will take care of excluding the parent directory, and # we're back to a happy case. But, if that's not the case, we need to # fire off a warning telling the user the path can't be excluded. Puppet.debug "Vcsrepo[#{@resource.name}]: Need to handle #{path} removal specially" File.delete(path) if Dir.entries(File.dirname(path)).sort != SKIP_DIRS Puppet.warning "Unable to exclude #{path} from Vcsrepo[#{@resource.name}]; update to subversion >= 1.7" end else Puppet.debug "Vcsrepo[#{@resource.name}]: Can remove #{path} directly using svn" args = buildargs.push('update', '--set-depth', 'exclude', path) svn_wrapper(*args) end # Keep walking up the parent directories of this include until we find # a non-empty folder, excluding as we go. while (path = path.rpartition(File::SEPARATOR)[0]) != '' entries = Dir.entries(path).sort break if entries != ['.', '..'] && entries != SKIP_DIRS args = buildargs.push('update', '--set-depth', 'exclude', path) svn_wrapper(*args) end end end def checkout_repository(source, path, revision, depth) args = buildargs.push('checkout') if revision args.push('-r', revision) end if @resource.value(:includes) # Make root checked out at empty depth to provide sparse directories args.push('--depth', 'empty') elsif depth args.push('--depth', depth) end args.push(source, path) svn_wrapper(*args) end def create_repository(path) args = ['create'] if @resource.value(:fstype) args.push('--fs-type', @resource.value(:fstype)) end args << path svnadmin(*args) end def update_owner set_ownership if @resource.value(:owner) || @resource.value(:group) end def update_includes(paths) at_path do args = buildargs.push('update') args.push('--depth', 'empty') if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end parents = paths.map { |path| File.dirname(path) } parents = make_include_paths(parents) args.push(*parents) svn_wrapper(*args) args = buildargs.push('update') if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end if @resource.value(:depth) args.push('--depth', @resource.value(:depth)) end args.push(*paths) svn_wrapper(*args) end end def make_include_paths(includes) includes.map { |inc| prefix = nil inc.split('/').map do |path| prefix = [prefix, path].compact.join('/') end }.flatten end def return_svn_client_version Facter.value('vcsrepo_svn_ver').dup end def validate_version svn_ver = return_svn_client_version raise "Includes option is not available for SVN versions < 1.6. Version installed: #{svn_ver}" if Gem::Version.new(svn_ver) < Gem::Version.new('1.6.0') end end diff --git a/spec/unit/puppet/provider/vcsrepo/svn_spec.rb b/spec/unit/puppet/provider/vcsrepo/svn_spec.rb index c598820..1ecab56 100644 --- a/spec/unit/puppet/provider/vcsrepo/svn_spec.rb +++ b/spec/unit/puppet/provider/vcsrepo/svn_spec.rb @@ -1,326 +1,326 @@ # frozen_string_literal: true require 'spec_helper' describe Puppet::Type.type(:vcsrepo).provider(:svn) do let(:resource) do Puppet::Type.type(:vcsrepo).new(name: 'test', ensure: :present, provider: :svn, path: '/tmp/vcsrepo') end let(:provider) { resource.provider } let(:test_paths) { ['path1/file1', 'path2/nested/deep/file2'] } let(:test_paths_parents) { ['path1', 'path2', 'path2/nested', 'path2/nested/deep'] } before :each do allow(Puppet::Util).to receive(:which).with('svn').and_return('/usr/bin/svn') end describe 'creation/checkout' do context 'with source and revision' do it "executes 'svn checkout' with a revision" do resource[:source] = 'exists' resource[:revision] = '1' expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', '-r', resource.value(:revision), resource.value(:source), resource.value(:path)) provider.create end end context 'with source' do it "justs execute 'svn checkout' without a revision" do resource[:source] = 'exists' expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', resource.value(:source), resource.value(:path)) provider.create end end context 'with fstype' do it "executes 'svnadmin create' with an '--fs-type' option" do resource[:fstype] = 'ext4' expect(provider).to receive(:svnadmin).with('create', '--fs-type', resource.value(:fstype), resource.value(:path)) provider.create end end context 'without fstype' do it "executes 'svnadmin create' without an '--fs-type' option" do expect(provider).to receive(:svnadmin).with('create', resource.value(:path)) provider.create end end context 'with depth' do it "executes 'svn checkout' with a depth" do resource[:source] = 'exists' resource[:depth] = 'infinity' expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', '--depth', 'infinity', resource.value(:source), resource.value(:path)) provider.create end end context 'with trust_server_cert' do it "executes 'svn checkout' without a trust-server-cert" do resource[:source] = 'exists' resource[:trust_server_cert] = false expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', resource.value(:source), resource.value(:path)) provider.create end it "executes 'svn checkout' with a trust-server-cert" do resource[:source] = 'exists' resource[:trust_server_cert] = true expect(provider).to receive(:svn_wrapper).with('--non-interactive', '--trust-server-cert', 'checkout', resource.value(:source), resource.value(:path)) provider.create end end context 'with specific include paths' do it 'raises an error when trying to make a repo' do resource[:includes] = test_paths expect { provider.create }.to raise_error(Puppet::Error, %r{Specifying include paths on a nonexistent repo.}) end it 'performs a sparse checkout' do resource[:source] = 'exists' resource[:includes] = test_paths expect(Dir).to receive(:chdir).with('/tmp/vcsrepo').once.and_yield expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', '--depth', 'empty', resource.value(:source), resource.value(:path)) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '--depth', 'empty', *test_paths_parents) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', *resource[:includes]) provider.create end it 'performs a sparse checkout at a specific revision' do resource[:source] = 'exists' resource[:revision] = 1 resource[:includes] = test_paths expect(Dir).to receive(:chdir).with('/tmp/vcsrepo').once.and_yield expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', '-r', resource.value(:revision), '--depth', 'empty', resource.value(:source), resource.value(:path)) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '--depth', 'empty', '-r', resource.value(:revision), *test_paths_parents) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '-r', resource.value(:revision), *resource[:includes]) provider.create end it 'performs a sparse checkout with a specific depth' do resource[:source] = 'exists' resource[:depth] = 'files' resource[:includes] = test_paths expect(Dir).to receive(:chdir).with('/tmp/vcsrepo').once.and_yield expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', '--depth', 'empty', resource.value(:source), resource.value(:path)) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '--depth', 'empty', *test_paths_parents) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '--depth', resource.value(:depth), *resource[:includes]) provider.create end it 'performs a sparse checkout at a specific depth and revision' do resource[:source] = 'exists' resource[:revision] = 1 resource[:depth] = 'files' resource[:includes] = test_paths expect(Dir).to receive(:chdir).with('/tmp/vcsrepo').once.and_yield expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'checkout', '-r', resource.value(:revision), '--depth', 'empty', resource.value(:source), resource.value(:path)) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '--depth', 'empty', '-r', resource.value(:revision), *test_paths_parents) expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '-r', resource.value(:revision), '--depth', resource.value(:depth), *resource[:includes]) provider.create end end end describe 'destroying' do it 'removes the directory' do expect_rm_rf provider.destroy end end describe 'checking existence' do it "runs `svn info` on the path when there's a source" do resource[:source] = 'dummy' expect_directory?(true, resource.value(:path)) expect(provider).to receive(:svn_wrapper).with('info', resource[:path]) provider.exists? end it "runs `svnlook uuid` on the path when there's no source" do expect_directory?(true, resource.value(:path)) expect(provider).to receive(:svnlook).with('uuid', resource[:path]) provider.exists? end end describe 'checking the revision property' do before(:each) do allow(provider).to receive(:svn_wrapper).with('--non-interactive', 'info').and_return(fixture(:svn_info)) end it "uses 'svn info'" do expect_chdir expect(provider.revision).to eq('4') # From 'Revision', not 'Last Changed Rev' end end describe 'setting the revision property' do let(:revision) { '30' } context 'with conflict' do it "uses 'svn update'" do resource[:conflict] = 'theirs-full' expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '-r', revision, '--accept', resource.value(:conflict)) provider.revision = revision end end context 'without conflict' do it "uses 'svn update'" do expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'update', '-r', revision) provider.revision = revision end end end describe 'setting the revision property and repo source' do let(:revision) { '30' } context 'with conflict' do it "uses 'svn switch'" do resource[:source] = 'an-unimportant-value' resource[:conflict] = 'theirs-full' expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'switch', '-r', revision, 'an-unimportant-value', '--accept', resource.value(:conflict)) provider.revision = revision end end context 'without conflict' do it "uses 'svn switch' - variation one" do resource[:source] = 'an-unimportant-value' expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'switch', '-r', revision, 'an-unimportant-value') provider.revision = revision end it "uses 'svn switch' - variation two" do resource[:source] = 'an-unimportant-value' resource[:revision] = '30' expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'switch', '-r', resource.value(:revision), 'an-unimportant-value') provider.source = resource.value(:source) end end end describe 'checking the source property' do before(:each) do allow(provider).to receive(:svn_wrapper).with('--non-interactive', 'info').and_return(fixture(:svn_info)) end it "uses 'svn info'" do expect_chdir expect(provider.source).to eq('http://example.com/svn/trunk') # From URL end end describe 'checking the basic_auth properties' do context 'when basic_auth_username is set and basic_auth_password is not set' do it 'fails' do resource[:source] = 'an-unimportant-value' resource[:basic_auth_username] = 'dummy_user' expect { provider.create }.to raise_error RuntimeError, %r{you must specify the HTTP basic authentication password.+}i end end context 'when basic_auth_username is not set and basic_auth_password is set' do it 'fails' do resource[:source] = 'an-unimportant-value' resource[:basic_auth_password] = 'dummy_pass' expect { provider.create }.to raise_error RuntimeError, %r{you must specify the HTTP .+username.*}i end end context 'when basic_auth_password is Sensitive' do let(:resource) do Puppet::Type.type(:vcsrepo).new(name: 'test', ensure: :present, provider: :svn, path: '/tmp/vcsrepo', source: 'an-unimportant-value', sensitive_parameters: [:basic_auth_password], basic_auth_username: 'dummy_user', basic_auth_password: Puppet::Pops::Types::PSensitiveType::Sensitive.new('dummy_pass')) end it 'works' do expect(provider).to receive(:svn_wrapper).with('--non-interactive', '--username', resource.value(:basic_auth_username), - '--password', resource.value(:basic_auth_password).unwrap, '--no-auth-cache', + '--password', resource.value(:basic_auth_password), '--no-auth-cache', 'checkout', resource.value(:source), resource.value(:path)) provider.create end end context 'when basic_auth_password contains non-ASCII characters' do it 'fails' do resource[:source] = 'an-important-value' resource[:basic_auth_username] = 'dummy_user' resource[:basic_auth_password] = 'ÙöØÓqþBÐh¦¹XH8«' expect { provider.create }.to raise_error RuntimeError, %r{The password can not contain non-ASCII characters} end end context 'when basic_auth_password contains only ASCII characters' do it 'works' do resource[:source] = 'an-important-value' resource[:basic_auth_username] = 'dummy_user' resource[:basic_auth_password] = 'dummy_pass' provider.create end end end describe 'setting the source property' do context 'with conflict' do it "uses 'svn switch'" do resource[:source] = 'http://example.com/svn/tags/1.0' resource[:conflict] = 'theirs-full' expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'switch', '--accept', resource.value(:conflict), resource.value(:source)) provider.source = resource.value(:source) end end context 'without conflict' do it "uses 'svn switch'" do resource[:source] = 'http://example.com/svn/tags/1.0' expect_chdir expect(provider).to receive(:svn_wrapper).with('--non-interactive', 'switch', resource.value(:source)) provider.source = resource.value(:source) end end end end