Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add elastic_stack_keystore resource to handle keystore files for both elasticsearch and kibana #71

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ba5e4dc
Add `elastic_stack_keystore` resource type to handle keystore file fo…
Dec 19, 2023
63feb6f
Update REFERENCE.md
Dec 19, 2023
0ca72d5
Add rspec unit test for `elastic_stack_keystore` type
Dec 19, 2023
751f659
Remove trailing whitespaces
Dec 19, 2023
809db6c
Use single-quoted strings when possible
Dec 20, 2023
5414935
Replace the legacy fact `osfamily` (deprecated) by the nested `os.fam…
Dec 20, 2023
c49f5f1
Add space after comma
Dec 20, 2023
da4fc03
Add space after comma
Dec 20, 2023
6e1ad26
Remove useless comment
Dec 20, 2023
b4250f7
Fix typo error
Dec 20, 2023
3cfb0d2
Add space missing after comma
Dec 20, 2023
3bcf5bc
Use _ to indicate that the var won't be used
Dec 20, 2023
594ec3e
Convert if nested inside else to elsif
Dec 20, 2023
654d72e
Pass a block to to_h instead of calling map.to_h
Dec 20, 2023
7b21f30
Use ternary operator in variable assignment and comparison
Dec 20, 2023
6daac95
Do not interpolate string
Dec 20, 2023
1ab1244
Remove tabs
Dec 20, 2023
fad2d79
Rename has_passwd? to passwd?
Dec 20, 2023
22b0820
Use %w for an array of words
Dec 20, 2023
b8c48eb
Redundant self assignment detected. Method concat modifies its receiv…
Dec 20, 2023
0fb187b
Simplify with a ternary operator
Dec 20, 2023
04caeb7
Use ternary operator to avoid if block
Dec 20, 2023
5bd9f68
Simplify with a single-line (`unless` statement)
Dec 20, 2023
78dd241
Simplify if statement
Dec 20, 2023
9a4f983
Simplify if statement
Dec 20, 2023
bfdc006
Simplify if statement using ternary operator
Dec 20, 2023
e5a9501
Use %r around regular expression
Dec 20, 2023
0619a4e
Merging nested conditions
Dec 20, 2023
b322191
Merging nested conditions into outer unless conditions
Dec 20, 2023
f7f4935
Fix error when running Puppet: "stack level too deep"
Dec 20, 2023
fb62c40
Convert if nested inside else to elsif
Dec 20, 2023
2ff3981
Fix variable `settings` used in void context
Dec 20, 2023
a390183
Require `spec_helper` (instead of `spec_helper_rspec`)
Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

* [`elastic_stack::repo`](#elastic_stack--repo): Set up the package repository for Elastic Stack components

### Resource types

* [`elastic_stack_keystore`](#elastic_stack_keystore): Manages a keystore settings file (for either Elasticserach or Kibana service.

## Classes

### <a name="elastic_stack--repo"></a>`elastic_stack::repo`
Expand Down Expand Up @@ -81,3 +85,61 @@ The base url for the repo path

Default value: `undef`

## Resource types

### <a name="elastic_stack_keystore"></a>`elastic_stack_keystore`

Manages a keystore settings file (for either Elasticserach or Kibana service.

#### Properties

The following properties are available in the `elastic_stack_keystore` type.

##### `ensure`

Valid values: `present`, `absent`

The basic property that the resource should be in.

Default value: `present`

##### `password`

Password to protect keystore.

Default value: `''`

##### `settings`

A key/value hash of settings names and values.

#### Parameters

The following parameters are available in the `elastic_stack_keystore` type.

* [`provider`](#-elastic_stack_keystore--provider)
* [`purge`](#-elastic_stack_keystore--purge)
* [`service`](#-elastic_stack_keystore--service)

##### <a name="-elastic_stack_keystore--provider"></a>`provider`

The specific backend to use for this `elastic_stack_keystore` resource. You will seldom need to specify this --- Puppet
will usually discover the appropriate provider for your platform.

##### <a name="-elastic_stack_keystore--purge"></a>`purge`

Valid values: `true`, `false`, `yes`, `no`

Whether to proactively remove settings that exist in the keystore but
are not present in this resource's settings.

Default value: `false`

##### <a name="-elastic_stack_keystore--service"></a>`service`

Valid values: `elasticsearch`, `kibana`

Service that manages the keystore (either "elasticsearch" or "kibana").

Default value: `elasticsearch`

313 changes: 313 additions & 0 deletions lib/puppet/provider/elastic_stack_keystore/ruby.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
# frozen_string_literal: true

Puppet::Type.type(:elastic_stack_keystore).provide(
:elastic_stack_keystore
) do
desc 'Provider for both `elasticsearch-keystore` and `kibana-keystore` based secret management.'

mk_resource_methods

def self.defaults_dir
@defaults_dir ||= case Facter.value(:os)['family']
when 'RedHat'
'/etc/sysconfig'
else
'/etc/default'
end
end

def self.root_dir
@root_dir ||= case Facter.value(:os)['family']
when 'OpenBSD'
'/usr/local'
else
'/usr/share'
end
end

def self.home_dir_kibana
@home_dir_kibana ||= File.join(root_dir, 'kibana')
end

def self.home_dir_elasticsearch
@home_dir_elasticsearch ||= File.join(root_dir, 'elasticsearch')
end

def self.elastic_keystore_password_file
keystore_env = get_envvar('elasticsearch', 'ES_KEYSTORE_PASSPHRASE_FILE')
@elastic_keystore_password_file ||= keystore_env.empty? ? "#{configdir('elasticsearch')}/.elasticsearch-keystore-password" : keystore_env
end

def self.elastic_keystore_password(password = '')
if File.file?(elastic_keystore_password_file)
@elastic_keystore_password ||= File.open(elastic_keystore_password_file, &:readline).strip
else
@elastic_keystore_password = password.empty? ? @elastic_keystore_password : password
end
end

def self.elastic_keystore_password_file_bak
@elastic_keystore_password_file_bak ||= "#{elastic_keystore_password_file}.puppet-bak"
end

def self.elastic_keystore_password_bak
@elastic_keystore_password_bak ||= File.file?(elastic_keystore_password_file_bak) ? File.open(elastic_keystore_password_file_bak, &:readline).strip : ''
end

attr_accessor :defaults_dir, :root_dir, :home_dir_kibana, :home_dir_elasticsearch, :elastic_keystore_password_file, :elastic_keystore_password, :elastic_keystore_password_file_bak, :elastic_keystore_password_bak

optional_commands kibana_keystore: "#{home_dir_kibana}/bin/kibana-keystore"
optional_commands elasticsearch_keystore: "#{home_dir_elasticsearch}/bin/elasticsearch-keystore"

def self.run_keystore(args, service, stdin = nil)
options = {
uid: service.to_s,
gid: service.to_s,
failonfail: true
}

password = case service
when 'elasticsearch'
File.file?(elastic_keystore_password_file_bak) ? elastic_keystore_password_bak : elastic_keystore_password
else
''
end

cmd = [command("#{service}_keystore")]
if args[0] == 'create' || args[0] == 'has-passwd'
options[:failonfail] = false
options[:combine] = true
elsif args[0] == 'passwd'
options[:combine] = true
stdin = File.file?(elastic_keystore_password_file_bak) ? "#{elastic_keystore_password_bak}\n#{elastic_keystore_password}\n#{elastic_keystore_password}" : "#{elastic_keystore_password}\n#{elastic_keystore_password}"
end

unless args[0] == 'passwd' || args[0] == 'has-passwd'
stdin = stdin.nil? ? password : "#{password}\n#{stdin}"
end

unless stdin.nil?
stdinfile = Tempfile.new("#{service}-keystore")
stdinfile << stdin
stdinfile.flush
options[:stdinfile] = stdinfile.path
end

begin
stdout = execute(cmd + args, options)
ensure
unless stdin.nil?
stdinfile.close
stdinfile.unlink
end
end

if stdout.exitstatus.zero?
stdout
else
options[:failonfail] ? raise(Puppet::Error, stdout) : stdout
end
end

def self.present_keystores(configdir, service, password = '')
keystore_file = File.join(configdir, "#{service}.keystore")
if File.file?(keystore_file)
current_password = case service
when 'elasticsearch'
if passwd?(service) && File.file?(elastic_keystore_password_file_bak)
elastic_keystore_password_bak
elsif passwd?(service)
elastic_keystore_password(password.value)
else
elastic_keystore_password(password.value)
''
end
else
''
end
settings = {}
run_keystore(['list'], service).split("\n").each do |setting|
settings[setting] = service == 'kibana' ? '' : run_keystore(['show', setting], service)
end
[{
name: service,
ensure: :present,
provider: name,
settings: settings,
password: current_password,
}]
else
[]
end
end

def self.configdir(service)
dir = get_envvar(service, '(ES|KBN)_PATH_CONF')
if dir.empty?
File.join('/etc', service)
else
dir
end
end

def self.get_envvar(service, env)
defaults_file = File.join(defaults_dir, service)
val = ''
if File.file?(defaults_file)
File.readlines(defaults_file).each do |line|
next if line =~ %r{^#}

key, value = line.split '='
val = value.gsub(%r{"}, '').strip if key =~ %r{#{env}}
end
end
val
end

def self.instances(password = '')
keystores = []
%w[kibana elasticsearch].each do |service|
keystores.concat(present_keystores(configdir(service), service, password))
end
keystores.map do |keystore|
new keystore
end
end

def self.passwd?(service)
has_passwd = run_keystore(['has-passwd'], service).split("\n").last
has_passwd.match?(%r{^Keystore is password-protected})
end

def self.keystore_password_management(service)
if passwd?(service)
run_keystore(['passwd'], service) unless elastic_keystore_password_bak.strip.empty? || elastic_keystore_password == elastic_keystore_password_bak
else
run_keystore(['passwd'], service) unless elastic_keystore_password.empty?
end
end

def self.prefetch(resources)
password = resources.key?(:elasticsearch) ? resources[:elasticsearch].parameters[:password] : ''
keystores = instances(password)
resources.each_key do |name|
provider = keystores.find { |keystore| keystore.name.to_sym == name }
resources[name].provider = provider if provider
end
end

def initialize(value = {})
super(value)
@property_flush = {}
end

def flush
configdir = self.class.configdir(resource[:service].to_s)
service = resource[:service].to_s

case @property_flush[:ensure]
when :present
debug(self.class.run_keystore(['create', '-s'], service, 'N'))
@property_flush[:settings] = resource[:settings]
when :absent
File.delete(File.join([
configdir, "#{resource[:service]}.keystore"
]))
return
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.key?(:settings) && !(@property_flush[:settings].empty? && @property_hash.nil? && @property_hash[:settings].nil?)
# Flush properties that _should_ be present
@property_flush[:settings].each do |setting, value|
next if @property_hash.key?(:settings) && @property_hash[:settings].key?(setting) \
&& @property_hash[:settings][setting] == value

args = ['add', '--force']
args << '--stdin' if service == 'kibana'
args << setting
debug(self.class.run_keystore(args, service, value))
end

# Remove properties that are no longer present
if resource[:purge]
(@property_hash[:settings].keys.sort - @property_flush[:settings].keys.sort).each do |setting|
debug(self.class.run_keystore(
['remove', setting], service
))
end
end
end

keystore_settings = {}
self.class.run_keystore(['list'], service).split("\n").each do |setting|
keystore_settings[setting] = service == 'kibana' ? '' : self.class.run_keystore(['show', setting], service)
end

# if service == 'elasticsearch' && @property_flush.key?(:password)
if service == 'elasticsearch'
# set and update keystore password if needed
self.class.keystore_password_management(service)
# unlink backup file containing keystore password (synced)
File.unlink(self.class.elastic_keystore_password_file_bak) if File.file?(self.class.elastic_keystore_password_file_bak)
end

@property_hash = {
name: service,
ensure: :present,
provider: resource[:name],
settings: keystore_settings,
password: self.class.elastic_keystore_password,
}
end

# 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

# settings property setter
#
# @return [String] password
def password=(new_password)
@property_flush[:password] = new_password
end

# settings property getter
#
# @return [Hash] password
def password
@property_hash[:password]
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
Loading