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