diff --git a/lib/puppet/provider/elastic_user_command.rb b/lib/puppet/provider/elastic_user_command.rb index 35ef51d..b6ca8e7 100644 --- a/lib/puppet/provider/elastic_user_command.rb +++ b/lib/puppet/provider/elastic_user_command.rb @@ -1,123 +1,125 @@ # Parent provider for Elasticsearch Shield/X-Pack file-based user management # tools. class Puppet::Provider::ElasticUserCommand < Puppet::Provider attr_accessor :homedir # Elasticsearch's home directory. # # @return String def self.homedir @homedir ||= case Facter.value('osfamily') when 'OpenBSD' '/usr/local/elasticsearch' else '/usr/share/elasticsearch' end end # Run the user management command with specified tool arguments. def self.command_with_path(args, configdir = nil) options = { + :combine => true, :custom_environment => { 'ES_PATH_CONF' => configdir || '/etc/elasticsearch' - } + }, + :failonfail => true } execute( [command(:users_cli)] + (args.is_a?(Array) ? args : [args]), options ) end # Gather local file-based users into an array of Hash objects. def self.fetch_users begin output = command_with_path('list') rescue Puppet::ExecutionFailure => e debug("#fetch_users had an error: #{e.inspect}") return nil end debug("Raw command output: #{output}") output.split("\n").select { |u| # Keep only expected "user : role1,role2" formatted lines u[/^[^:]+:\s+\S+$/] }.map { |u| # Break into ["user ", " role1,role2"] u.split(':').first.strip }.map do |user| { :name => user, :ensure => :present, :provider => name } end end # Fetch an array of provider objects from the the list of local users. def self.instances fetch_users.map do |user| new user end end # Generic prefetch boilerplate. def self.prefetch(resources) instances.each do |prov| if (resource = resources[prov.name]) resource.provider = prov end end end def initialize(value = {}) super(value) @property_flush = {} end # Enforce the desired state for this user on-disk. def flush arguments = [] case @property_flush[:ensure] when :absent arguments << 'userdel' arguments << resource[:name] else arguments << 'useradd' arguments << resource[:name] arguments << '-p' << resource[:password] end self.class.command_with_path(arguments, resource[:configdir]) @property_hash = self.class.fetch_users.detect do |u| u[:name] == resource[:name] end end # Set this provider's `:ensure` property to `:present`. def create @property_flush[:ensure] = :present end def exists? @property_hash[:ensure] == :present end # Set this provider's `:ensure` property to `:absent`. def destroy @property_flush[:ensure] = :absent end # Manually set this user's password. def passwd self.class.command_with_path( [ 'passwd', resource[:name], '-p', resource[:password] ], resource[:configdir] ) end end diff --git a/lib/puppet/provider/elasticsearch_keystore/ruby.rb b/lib/puppet/provider/elasticsearch_keystore/ruby.rb index b21e78e..6233564 100644 --- a/lib/puppet/provider/elasticsearch_keystore/ruby.rb +++ b/lib/puppet/provider/elasticsearch_keystore/ruby.rb @@ -1,166 +1,167 @@ Puppet::Type.type(:elasticsearch_keystore).provide( :elasticsearch_keystore ) do desc 'Provider for `elasticsearch-keystore` based secret management.' def self.defaults_dir @defaults_dir ||= case Facter.value('osfamily') when 'RedHat' '/etc/sysconfig' else '/etc/default' end end def self.home_dir @home_dir ||= case Facter.value('osfamily') when 'OpenBSD' '/usr/local/elasticsearch' else '/usr/share/elasticsearch' end end attr_accessor :defaults_dir, :home_dir commands :keystore => "#{home_dir}/bin/elasticsearch-keystore" def self.run_keystore(args, instance, configdir = '/etc/elasticsearch', stdin = nil) options = { :custom_environment => { - 'ES_INCLUDE' => File.join(defaults_dir, "elasticsearch-#{instance}"), + 'ES_INCLUDE' => File.join(defaults_dir, "elasticsearch-#{instance}"), 'ES_PATH_CONF' => "#{configdir}/#{instance}" }, - :uid => 'elasticsearch', - :gid => 'elasticsearch' + :uid => 'elasticsearch', + :gid => 'elasticsearch', + :failonfail => true } unless stdin.nil? stdinfile = Tempfile.new('elasticsearch-keystore') stdinfile << stdin stdinfile.flush options[:stdinfile] = stdinfile.path end begin stdout = execute([command(:keystore)] + args, options) ensure unless stdin.nil? stdinfile.close stdinfile.unlink end end stdout.exitstatus.zero? ? stdout : raise(Puppet::Error, stdout) end def self.present_keystores Dir[File.join(%w[/ etc elasticsearch *])].select do |directory| File.exist? File.join(directory, 'elasticsearch.keystore') end.map do |instance| settings = run_keystore(['list'], File.basename(instance)).split("\n") { :name => File.basename(instance), :ensure => :present, :provider => name, :settings => settings } end end def self.instances present_keystores.map do |keystore| new keystore end end def self.prefetch(resources) instances.each do |prov| if (resource = resources[prov.name]) resource.provider = prov end end end def initialize(value = {}) super(value) @property_flush = {} end # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity def flush case @property_flush[:ensure] when :present debug(self.class.run_keystore(['create'], resource[:name], resource[:configdir])) @property_flush[:settings] = resource[:settings] when :absent File.delete(File.join([ '/', 'etc', 'elasticsearch', resource[:instance], 'elasticsearch.keystore' ])) end # Note that since the property is :array_matching => :all, we have to # expect that the hash is wrapped in an array. if @property_flush[:settings] and not @property_flush[:settings].first.empty? # Flush properties that _should_ be present @property_flush[:settings].first.each_pair do |setting, value| next unless @property_hash[:settings].nil? \ or not @property_hash[:settings].include? setting debug(self.class.run_keystore( ['add', '--force', '--stdin', setting], resource[:name], resource[:configdir], value )) end # Remove properties that are no longer present if resource[:purge] and not (@property_hash.nil? or @property_hash[:settings].nil?) (@property_hash[:settings] - @property_flush[:settings].first.keys).each do |setting| debug(self.class.run_keystore( ['remove', setting], resource[:name], resource[:configdir] )) end end end @property_hash = self.class.present_keystores.detect do |u| u[:name] == resource[:name] end end # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity # settings property setter # # @return [Hash] settings def settings=(new_settings) @property_flush[:settings] = new_settings end # settings property getter # # @return [Hash] settings def settings @property_hash[:settings] end # Sets the ensure property in the @property_flush hash. # # @return [Symbol] :present def create @property_flush[:ensure] = :present end # Determine whether this resource is present on the system. # # @return [Boolean] def exists? @property_hash[:ensure] == :present end # Set flushed ensure property to absent. # # @return [Symbol] :absent def destroy @property_flush[:ensure] = :absent end end diff --git a/spec/unit/provider/elasticsearch_keystore/elasticsearch_keystore_spec.rb b/spec/unit/provider/elasticsearch_keystore/elasticsearch_keystore_spec.rb index 341d79e..a679b50 100644 --- a/spec/unit/provider/elasticsearch_keystore/elasticsearch_keystore_spec.rb +++ b/spec/unit/provider/elasticsearch_keystore/elasticsearch_keystore_spec.rb @@ -1,157 +1,161 @@ require 'spec_helper_rspec' shared_examples 'keystore instance' do |instance| describe "instance #{instance}" do subject { described_class.instances.find { |x| x.name == instance } } it { expect(subject.exists?).to be_truthy } it { expect(subject.name).to eq(instance) } it { expect(subject.settings) .to eq(['node.name', 'cloud.aws.access_key']) } end end describe Puppet::Type.type(:elasticsearch_keystore).provider(:elasticsearch_keystore) do let(:executable) { '/usr/share/elasticsearch/bin/elasticsearch-keystore' } let(:instances) { [] } before do Facter.clear Facter.add('osfamily') { setcode { 'Debian' } } allow(described_class) .to receive(:command) .with(:keystore) .and_return(executable) allow(File).to receive(:exist?) .with('/etc/elasticsearch/scripts/elasticsearch.keystore') .and_return(false) end describe 'instances' do before do allow(Dir).to receive(:[]) .with('/etc/elasticsearch/*') .and_return((['scripts'] + instances).map do |directory| "/etc/elasticsearch/#{directory}" end) instances.each do |instance| instance_dir = "/etc/elasticsearch/#{instance}" defaults_file = "/etc/default/elasticsearch-#{instance}" allow(File).to receive(:exist?) .with("#{instance_dir}/elasticsearch.keystore") .and_return(true) expect(described_class) .to receive(:execute) .with( [executable, 'list'], :custom_environment => { 'ES_INCLUDE' => defaults_file, 'ES_PATH_CONF' => "/etc/elasticsearch/#{instance}" }, - :uid => 'elasticsearch', :gid => 'elasticsearch' + :uid => 'elasticsearch', + :gid => 'elasticsearch', + :failonfail => true ) .and_return( Puppet::Util::Execution::ProcessOutput.new( "node.name\ncloud.aws.access_key\n", 0 ) ) end end it 'should have an instance method' do expect(described_class).to respond_to(:instances) end context 'without any keystores' do it 'should return no resources' do expect(described_class.instances.size).to eq(0) end end context 'with one instance' do let(:instances) { ['es-01'] } it { expect(described_class.instances.length).to eq(instances.length) } include_examples 'keystore instance', 'es-01' end context 'with multiple instances' do let(:instances) { ['es-01', 'es-02'] } it { expect(described_class.instances.length).to eq(instances.length) } include_examples 'keystore instance', 'es-01' include_examples 'keystore instance', 'es-02' end end # of describe instances describe 'prefetch' do it 'should have a prefetch method' do expect(described_class).to respond_to :prefetch end end describe 'flush' do let(:provider) { described_class.new(:name => 'es-03') } let(:resource) do Puppet::Type.type(:elasticsearch_keystore).new( :name => 'es-03', :provider => provider ) end it 'creates the keystore' do expect(described_class).to( receive(:execute) .with( [executable, 'create'], :custom_environment => { 'ES_INCLUDE' => '/etc/default/elasticsearch-es-03', 'ES_PATH_CONF' => '/etc/elasticsearch/es-03' }, - :uid => 'elasticsearch', :gid => 'elasticsearch' + :uid => 'elasticsearch', + :gid => 'elasticsearch', + :failonfail => true ) .and_return(Puppet::Util::Execution::ProcessOutput.new('', 0)) ) resource[:ensure] = :present provider.create provider.flush end it 'deletes the keystore' do expect(File).to( receive(:delete) .with(File.join(%w[/ etc elasticsearch es-03 elasticsearch.keystore])) ) resource[:ensure] = :absent provider.destroy provider.flush end it 'updates settings' do settings = { 'cloud.aws.access_key' => 'AKIAFOOBARFOOBAR', 'cloud.aws.secret_key' => 'AKIAFOOBARFOOBAR' } settings.each do |setting, value| expect(provider.class).to( receive(:run_keystore) .with(['add', '--force', '--stdin', setting], 'es-03', '/etc/elasticsearch', value) .and_return(Puppet::Util::Execution::ProcessOutput.new('', 0)) ) end # Note that the settings hash is passed in wrapped in an array to mimic # the behavior in real-world puppet runs. resource[:ensure] = :present resource[:settings] = [settings] provider.settings = [settings] provider.flush end end # of describe flush end # of describe Puppet::Type elasticsearch_keystore