diff --git a/REFERENCE.md b/REFERENCE.md index aaeccaa..1c26b6a 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -6,6 +6,10 @@ ### Functions +* [`extlib::aws::rds::db_instances`](#extlib--aws--rds--db_instances): Wraps Amazon RDS DescribeDBInstances to return detailed information on one or all RDS database instances. +* [`extlib::aws::rds_master_secret`](#extlib--aws--rds_master_secret): Convenience wrapper function for retrieving the AWS managed master secret of an RDS database instance. +* [`extlib::aws::region`](#extlib--aws--region): Returns the AWS region of the host running this function, read from its EC2 instance metadata (IMDS). +* [`extlib::aws::secretsmanager::secret_value`](#extlib--aws--secretsmanager--secret_value): Retrieves and parses an AWS Secrets Manager secret * [`extlib::cache_data`](#extlib--cache_data): Retrieves data from a cache file, or creates it with supplied data if the file doesn't exist * [`extlib::cidr_to_netmask`](#extlib--cidr_to_netmask): Converts an CIDR address of the form 192.168.0.1/24 into its netmask. * [`extlib::cidr_to_network`](#extlib--cidr_to_network): Converts a CIDR address of the form 2001:DB8::/32 or 192.0.2.0/24 into their network address (also known as net address) @@ -44,6 +48,120 @@ Based on https://github.com/mmckinst/puppet-hash2stuff/blob/master/lib/puppet/pa ## Functions +### `extlib::aws::rds::db_instances` + +Type: Ruby 4.x API + +This function queries the Amazon RDS API to retrieve information on RDS +database instances. + +Currently, it only supports querying the instances using the IAM role +permissions of the EC2 instance running the function, (usually your +puppetserver unless the function call is `Deferred`), and it only supports +querying the instances in the same account as it is being run. + +#### `extlib::aws::rds::db_instances(Optional[String[1]] $db_instance_identifier, Optional[Variant[Undef, String[1]]] $region)` + +This function queries the Amazon RDS API to retrieve information on RDS +database instances. + +Currently, it only supports querying the instances using the IAM role +permissions of the EC2 instance running the function, (usually your +puppetserver unless the function call is `Deferred`), and it only supports +querying the instances in the same account as it is being run. + +Returns: `Variant[Array[Hash],Hash]` Returns a hash containing the DB instance data, or an Array of such hashes if the `db_instance_identifier` parameter was not specified. + +##### `db_instance_identifier` + +Data type: `Optional[String[1]]` + +The RDS instance identifier or ARN of the DB instance. If omitted, returns an Array containing details of _all_ instances. + +##### `region` + +Data type: `Optional[Variant[Undef, String[1]]]` + +The AWS region as used when creating the API client. If omitted (or explicitly passed `undef`), the region will be automatically looked up from the metadata of the EC2 instance running the function. + +### `extlib::aws::rds_master_secret` + +Type: Puppet Language + +Convenience wrapper function for retrieving the AWS managed master secret of an RDS database instance. + +#### `extlib::aws::rds_master_secret(String[1] $db_instance_identifier, Optional[String[1]] $region = undef)` + +The extlib::aws::rds_master_secret function. + +Returns: `Hash` The DB instance master secret hash, containing details such as the `username` and `password` depending on RDS instance type. + +##### `db_instance_identifier` + +Data type: `String[1]` + +The RDS instance identifier or ARN + +##### `region` + +Data type: `Optional[String[1]]` + +Optionally specify your AWS region. If not given, the `extlib::aws::region` function will be used to fetch the region. + +### `extlib::aws::region` + +Type: Ruby 4.x API + +This function is primarily intended to be used internally by other +`extlib::aws` functions. It takes no parameters but depends on the EC2 +Instance metadata service (IMDS) being `enabled`, (ie on your EC2 based +puppetserver or your agent if run as a `Deferred` function.) + +#### `extlib::aws::region()` + +This function is primarily intended to be used internally by other +`extlib::aws` functions. It takes no parameters but depends on the EC2 +Instance metadata service (IMDS) being `enabled`, (ie on your EC2 based +puppetserver or your agent if run as a `Deferred` function.) + +Returns: `String[1]` Returns an AWS region. + +### `extlib::aws::secretsmanager::secret_value` + +Type: Ruby 4.x API + +This function queries the Amazon SecretsManager API to retrieve a secret +based on the ARN provided. + +Currently, it only supports querying the instances using the IAM role +permissions of the EC2 instance running the function, (usually your +puppetserver unless the function call is `Deferred`), and it only supports +fetching secrets from the same account as the function is being run. + +#### `extlib::aws::secretsmanager::secret_value(String[1] $secret_arn, Optional[Variant[Undef, String[1]]] $region)` + +This function queries the Amazon SecretsManager API to retrieve a secret +based on the ARN provided. + +Currently, it only supports querying the instances using the IAM role +permissions of the EC2 instance running the function, (usually your +puppetserver unless the function call is `Deferred`), and it only supports +fetching secrets from the same account as the function is being run. + +Returns: `Variant[Sensitive[String[1]], Hash, Sensitive[Hash]]` Returns the secret. For plain text secrets, the function will return a `Sensitive[String]`. For key:value secrets, the secret JSON will be decoded. If the secret contains a `password` field, this will be returned as a `Sensitive[String]` within the `Hash` returned. If there isn't a `password` field, the complete hash will be returned wrapped in `Sensitive`. + +##### `secret_arn` + +Data type: `String[1]` + +The ARN of the secret to fetch. + +##### `region` + +Data type: `Optional[Variant[Undef, String[1]]]` + +The AWS region as used when creating the API client. If omitted (or explicitly passed `undef`), the region will be automatically looked up from the metadata of the EC2 instance running the function. + ### `extlib::cache_data` Type: Ruby 4.x API diff --git a/functions/aws/rds_master_secret.pp b/functions/aws/rds_master_secret.pp new file mode 100644 index 0000000..12baba0 --- /dev/null +++ b/functions/aws/rds_master_secret.pp @@ -0,0 +1,22 @@ +# @summary Convenience wrapper function for retrieving the AWS managed master secret of an RDS database instance. +# +# @param db_instance_identifier +# The RDS instance identifier or ARN +# @param region +# Optionally specify your AWS region. If not given, the `extlib::aws::region` function will be used to fetch the region. +# @return The DB instance master secret hash, containing details such as the `username` and `password` depending on RDS instance type. +function extlib::aws::rds_master_secret ( + String[1] $db_instance_identifier, + Optional[String[1]] $region = undef, +) >> Hash { + $rds_db_instance = extlib::aws::rds::db_instances($db_instance_identifier, $region) + $rds_master_user_secret = $rds_db_instance['master_user_secret'] + + unless $rds_master_user_secret =~ Hash { + fail("RDS DB instance '${db_instance_identifier}' has no AWS managed master secret (master_user_secret).") + } + + unless $rds_master_user_secret['secret_status'] == 'active' { fail('rds_master_user_secret was not in state `active`') } + + extlib::aws::secretsmanager::secret_value($rds_master_user_secret['secret_arn'], $region) +} diff --git a/lib/puppet/functions/extlib/aws/rds/db_instances.rb b/lib/puppet/functions/extlib/aws/rds/db_instances.rb new file mode 100644 index 0000000..13bfbb5 --- /dev/null +++ b/lib/puppet/functions/extlib/aws/rds/db_instances.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# @summary Wraps Amazon RDS DescribeDBInstances to return detailed information on one or all RDS database instances. +# +# This function queries the Amazon RDS API to retrieve information on RDS +# database instances. +# +# Currently, it only supports querying the instances using the IAM role +# permissions of the EC2 instance running the function, (usually your +# puppetserver unless the function call is `Deferred`), and it only supports +# querying the instances in the same account as it is being run. +Puppet::Functions.create_function(:'extlib::aws::rds::db_instances') do + # @param db_instance_identifier The RDS instance identifier or ARN of the DB instance. If omitted, returns an Array containing details of _all_ instances. + # @param region The AWS region as used when creating the API client. If omitted (or explicitly passed `undef`), the region will be automatically looked up from the metadata of the EC2 instance running the function. + # @return [Variant[Array[Hash],Hash]] Returns a hash containing the DB instance data, or an Array of such hashes if the `db_instance_identifier` parameter was not specified. + dispatch :db_instances do + optional_param 'String[1]', :db_instance_identifier + optional_param 'Variant[Undef, String[1]]', :region + return_type 'Variant[Array[Hash],Hash]' + end + + require 'json' + + def db_instances(db_instance_identifier = nil, region = nil) + begin + require 'aws-sdk-rds' + rescue LoadError => e + raise Puppet::Error, "extlib::aws::rds::db_instances requires the 'aws-sdk-rds' gem. (#{e.message})" + end + + region ||= call_function('extlib::aws::region') + client = Aws::RDS::Client.new(region: region) + + begin + resp = client.describe_db_instances(db_instance_identifier: db_instance_identifier) + instances = resp.each_page.flat_map(&:db_instances) + rescue Aws::RDS::Errors::DBInstanceNotFound => e + raise Puppet::Error, "RDS DB instance '#{db_instance_identifier}' not found: #{e.message}" + rescue Aws::Errors::ServiceError => e + raise Puppet::Error, "Error describing RDS DB instance(s): #{e.message}" + end + + if db_instance_identifier + # We should have *exactly* one db instance returned, so this is a sanity check. + raise Puppet::DevError, "RDS DB instance '#{db_instance_identifier}' not found?!" if instances.size != 1 + + # JSON.parse(JSON.dump(x)) is a convenient way to recursively convert all symbols into normal strings. + JSON.parse(JSON.dump(instances.first.to_h)) + else + instances.map do |instance| + JSON.parse(JSON.dump(instance.to_h)) + end + end + end +end diff --git a/lib/puppet/functions/extlib/aws/region.rb b/lib/puppet/functions/extlib/aws/region.rb new file mode 100644 index 0000000..0bc8775 --- /dev/null +++ b/lib/puppet/functions/extlib/aws/region.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# @summary Returns the AWS region of the host running this function, read from its EC2 instance metadata (IMDS). +# +# This function is primarily intended to be used internally by other +# `extlib::aws` functions. It takes no parameters but depends on the EC2 +# Instance metadata service (IMDS) being `enabled`, (ie on your EC2 based +# puppetserver or your agent if run as a `Deferred` function.) +Puppet::Functions.create_function(:'extlib::aws::region') do + # The host's region is constant for the life of the process, so we only ever + # query IMDS once and memoize the result process-wide. + @cached_region = nil + + class << self + attr_accessor :cached_region + end + + # @return [String[1]] Returns an AWS region. + dispatch :region do + return_type 'String[1]' + end + + def region + self.class.cached_region ||= lookup_region + end + + def lookup_region + begin + require 'aws-sdk-core' + rescue LoadError => e + raise Puppet::Error, "extlib::aws::region requires the 'aws-sdk-core' gem. (#{e.message})" + end + + begin + value = Aws::EC2Metadata.new.get('/latest/meta-data/placement/region') + rescue StandardError => e + raise Puppet::Error, "Unable to read AWS region from EC2 instance metadata: #{e.message}" + end + + raise Puppet::Error, 'EC2 instance metadata returned an empty region.' if value.nil? || value.strip.empty? + + value.strip + end +end diff --git a/lib/puppet/functions/extlib/aws/secretsmanager/secret_value.rb b/lib/puppet/functions/extlib/aws/secretsmanager/secret_value.rb new file mode 100644 index 0000000..dfc09b9 --- /dev/null +++ b/lib/puppet/functions/extlib/aws/secretsmanager/secret_value.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# @summary Retrieves and parses an AWS Secrets Manager secret +# +# This function queries the Amazon SecretsManager API to retrieve a secret +# based on the ARN provided. +# +# Currently, it only supports querying the instances using the IAM role +# permissions of the EC2 instance running the function, (usually your +# puppetserver unless the function call is `Deferred`), and it only supports +# fetching secrets from the same account as the function is being run. +Puppet::Functions.create_function(:'extlib::aws::secretsmanager::secret_value') do + # @param secret_arn The ARN of the secret to fetch. + # @param region The AWS region as used when creating the API client. If omitted (or explicitly passed `undef`), the region will be automatically looked up from the metadata of the EC2 instance running the function. + # @return [Variant[Sensitive[String[1]], Hash, Sensitive[Hash]]] Returns the secret. For plain text secrets, the function will return a `Sensitive[String]`. For key:value secrets, the secret JSON will be decoded. If the secret contains a `password` field, this will be returned as a `Sensitive[String]` within the `Hash` returned. If there isn't a `password` field, the complete hash will be returned wrapped in `Sensitive`. + dispatch :secret_value do + param 'String[1]', :secret_arn + optional_param 'Variant[Undef, String[1]]', :region + return_type 'Variant[Sensitive[String[1]], Hash, Sensitive[Hash]]' + end + + require 'json' + + def secret_value(secret_arn, region = nil) + begin + require 'aws-sdk-secretsmanager' + rescue LoadError => e + raise Puppet::Error, "extlib::aws::secretsmanager::secret_value requires the 'aws-sdk-secretsmanager' gem. (#{e.message})" + end + + region ||= call_function('extlib::aws::region') + client = Aws::SecretsManager::Client.new(region: region) + + begin + resp = client.get_secret_value(secret_id: secret_arn) + rescue Aws::SecretsManager::Errors::ResourceNotFoundException => e + raise Puppet::Error, "Secret '#{secret_arn}' not found: #{e.message}" + rescue Aws::Errors::ServiceError => e + raise Puppet::Error, "Error retrieving secret '#{secret_arn}': #{e.message}" + end + + payload = resp.secret_string + raise Puppet::Error, "Secret '#{secret_arn}' has no SecretString (binary secrets are not supported)" if payload.nil? + + begin + data = JSON.parse(payload) + rescue JSON::ParserError + data = nil + end + + # Anything that isn't a JSON object (a plain string, or a JSON scalar such + # as a number or boolean) is treated as a 'normal' string secret which we + # wrap in Sensitive and return. + return Puppet::Pops::Types::PSensitiveType::Sensitive.new(payload) unless data.is_a?(Hash) + + # Either wrap a `password` field if it exists, or the whole Hash otherwise + if data.key?('password') + data['password'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(data['password']) + else + data = Puppet::Pops::Types::PSensitiveType::Sensitive.new(data) + end + + data + end +end