diff --git a/README.md b/README.md index 5286896a..8f3d5744 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ to model the relationships between different components. ###Installing the aws module -1. Install the retries gem and the Amazon AWS Ruby SDK gem. +1. Install the retries gem and the Amazon AWS Ruby SDK gem. * If you're using open source Puppet, the SDK gem should be installed into the same Ruby used by Puppet. Install the gems with these commands: @@ -243,7 +243,7 @@ puppet apply tests/destroy.pp --test ### Managing resources from the command line -The module has basic `puppet resource` support, so you can manage AWS resources from the command line. +The module has basic `puppet resource` support, so you can manage AWS resources from the command line. For example, the following command lists all the security groups: @@ -300,6 +300,9 @@ You can use the aws module to audit AWS resources, launch autoscaling groups in * `ec2_vpc_subnet`: Sets up a VPC subnet. * `ec2_vpc_vpn`: Sets up an AWS Virtual Private Network. * `ec2_vpc_vpn_gateway`: Sets up a VPN gateway. +* `rds_db_parameter_group`: Allows read access to DB Parameter Groups. +* `rds_db_securitygroup`: Sets up an RDS DB Security Group. +* `rds_instance`: Sets up an RDS Database instance. * `route53_a_record`: Sets up a Route53 DNS record. * `route53_aaaa_record`: Sets up a Route53 DNS AAAA record. * `route53_cname_record`: Sets up a Route53 CNAME record. @@ -315,7 +318,7 @@ You can use the aws module to audit AWS resources, launch autoscaling groups in ####Type: ec2_instance #####`ensure` -Specifies the basic state of the resource. Valid values are 'present', 'absent', 'running', 'stopped'. +Specifies the basic state of the resource. Valid values are 'present', 'absent', 'running', 'stopped'. #####`name` *Required* The name of the instance. This is the value of the AWS Name tag. @@ -336,10 +339,10 @@ The name of the key pair associated with this instance. This must be an existing *Optional* Whether or not monitoring is enabled for this instance. This parameter is set at creation only; it is not affected by updates. Valid values are 'true', 'false'. Defaults to 'false'. #####`region` -*Required* The region in which to launch the instance. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). +*Required* The region in which to launch the instance. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). #####`image_id` -*Required* The image id to use for the instance. This parameter is set at creation only; it is not affected by updates. For more information, see AWS documentation on finding your [Amazon Machine Image (AMI)](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html). +*Required* The image id to use for the instance. This parameter is set at creation only; it is not affected by updates. For more information, see AWS documentation on finding your [Amazon Machine Image (AMI)](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html). #####`availability_zone` *Optional* The availability zone in which to place the instance. This parameter is set at creation only; it is not affected by updates. For valid availability zone codes, see [AWS Regions and Availability Zones](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). @@ -348,13 +351,13 @@ The name of the key pair associated with this instance. This must be an existing *Required* The type to use for the instance. This parameter is set at creation only; it is not affected by updates. See [Amazon EC2 Instances](http://aws.amazon.com/ec2/instance-types/) for available types. #####`private_ip_addresS` -*Optional* The private IP address for the instance. This parameter is set at creation only; it is not affected by updates. Must be a valid IPv4 address. +*Optional* The private IP address for the instance. This parameter is set at creation only; it is not affected by updates. Must be a valid IPv4 address. #####`associate_public_ip_address` *Optional* Whether to assign a public interface in a VPC. This parameter is set at creation only; it is not affected by updates. Valid values are 'true', 'false'. Defaults to 'false'. #####`subnet` -*Optional* The VPC subnet to attach the instance to. This parameter is set at creation only; it is not affected by updates. Accepts the name of the subnet; this is the value of the Name tag for the subnet. If you're describing the subnet in Puppet, then this value is the name of the resource. +*Optional* The VPC subnet to attach the instance to. This parameter is set at creation only; it is not affected by updates. Accepts the name of the subnet; this is the value of the Name tag for the subnet. If you're describing the subnet in Puppet, then this value is the name of the resource. #####`ebs_optimized` *Optional* Whether or not to use optimized storage for the instance. This parameter is set at creation only; it is not affected by updates. Valid values are 'true', 'false'. Defaults to 'false'. @@ -362,8 +365,8 @@ The name of the key pair associated with this instance. This must be an existing #####`instance_initiated_shutdown_behavior` *Optional* Whether the instance stops or terminates when you initiate shutdown from the instance. This parameter is set at creation only; it is not affected by updates. Valid values are 'stop', 'terminate'. Defaults to 'stop'. -#####`block_devices` -*Optional* A list of block devices to associate with the instance. This parameter is set at creation only; it is not affected by updates. Accepts an array of hashes with the device name and volume size specified: +#####`block_devices` +*Optional* A list of block devices to associate with the instance. This parameter is set at creation only; it is not affected by updates. Accepts an array of hashes with the device name and volume size specified: ~~~ block_devices => [ @@ -371,7 +374,7 @@ block_devices => [ device_name => '/dev/sda1', volume_size => 8, } -] +] ~~~ #####`instance_id` @@ -412,7 +415,7 @@ The Amazon Resource Name for the associated IAM profile. ##### `ingress` *Optional* Rules for ingress traffic. Accepts an array. - + ##### `tags` *Optional* The tags for the security group. Accepts a 'key => value' hash of tags. @@ -445,7 +448,7 @@ The Amazon Resource Name for the associated IAM profile. *Optional* The subnet in which the load balancer should be launched. This parameter is set at creation only; it is not affected by updates. Accepts an array of subnet names, i.e., the Name tags on the subnets. -#####`security_groups` +#####`security_groups` *Optional* The security groups to associate with the load balancer (VPC only). Accepts an array of security group names, i.e., the Name tag on the security groups. #####`availability_zones` @@ -498,7 +501,7 @@ The Amazon Resource Name for the associated IAM profile. *Required* The name of the auto scaling group. This is the value of the AWS Name tag. ##### `min_size` -*Required* The minimum number of instances in the group. +*Required* The minimum number of instances in the group. ##### `max_size` *Required* The maximum number of instances in the group. @@ -521,7 +524,7 @@ The Amazon Resource Name for the associated IAM profile. Specifies that basic state of the resource. Valid values are 'attached', 'detached'. #####`name` -*Required* The IP address of the Elastic IP. Accepts a valid IPv4 address of an already existing elastic IP. +*Required* The IP address of the Elastic IP. Accepts a valid IPv4 address of an already existing elastic IP. #####`region` *Required* The region in which the Elastic IP is found. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). @@ -551,7 +554,7 @@ Specifies that basic state of the resource. Valid values are 'attached', 'detach *Required* The type to use for the instances. This parameter is set at creation only; it is not affected by updates. See [Amazon EC2 Instances](http://aws.amazon.com/ec2/instance-types/) for available types. #####`image_id` -*Required* The image id to use for the instances. This parameter is set at creation only; it is not affected by updates. For more information, see AWS documentation on finding your [Amazon Machine Image (AMI)](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html). +*Required* The image id to use for the instances. This parameter is set at creation only; it is not affected by updates. For more information, see AWS documentation on finding your [Amazon Machine Image (AMI)](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html). #####`vpc` *Optional* A hint to specify the VPC. This is useful when detecting ambiguously named security groups that might exist in different VPCs, such as 'default'. This parameter is set at creation only; it is not affected by updates. @@ -562,13 +565,13 @@ Specifies that basic state of the resource. Valid values are 'attached', 'detach *Required* The name of the scaling policy. This is the value of the AWS Name tag. #####`scaling_adjustment` -*Required* The amount to adjust the size of the group by. Valid values depend on `adjustment_type` chosen. See [AWS Dynamic Scaling](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html) documentation. +*Required* The amount to adjust the size of the group by. Valid values depend on `adjustment_type` chosen. See [AWS Dynamic Scaling](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html) documentation. #####`region` *Required* The region in which to launch the policy. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). #####`adjustment_type` -*Required* The type of policy. Accepts a string specifying the policy adjustment type. For valid values, see AWS [Adjustment Type](http://docs.aws.amazon.com/AutoScaling/latest/APIReference/API_AdjustmentType.html) documentation. +*Required* The type of policy. Accepts a string specifying the policy adjustment type. For valid values, see AWS [Adjustment Type](http://docs.aws.amazon.com/AutoScaling/latest/APIReference/API_AdjustmentType.html) documentation. #####`auto_scaling_group` *Required* The name of the auto scaling group to attach the policy to. This is the value of the AWS Name tag. This parameter is set at creation only; it is not affected by updates. @@ -587,7 +590,7 @@ Specifies that basic state of the resource. Valid values are 'attached', 'detach #####`dhcp_options` *Optional* The name of DHCP option set to use for this VPC. This parameter is set at creation only; it is not affected by updates. -#####`instance_tenancy` +#####`instance_tenancy` *Optional* The supported tenancy options for instances in this VPC. This parameter is set at creation only; it is not affected by updates. Valid values are 'default', 'dedicated'. Defaults to 'default'. #####`tags` @@ -625,10 +628,10 @@ The type of customer gateway. The only currently supported value --- and the def *Optional* The region in which to assign the DHCP option set. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). #####`domain_name` -*Optional* The domain name for the DHCP options. This parameter is set at creation only; it is not affected by updates. Accepts any valid domain. +*Optional* The domain name for the DHCP options. This parameter is set at creation only; it is not affected by updates. Accepts any valid domain. -#####`domain_name_servers` -*Optional* A list of domain name servers to use for the DHCP options set. This parameter is set at creation only; it is not affected by updates. Accepts an array of domain server names. +#####`domain_name_servers` +*Optional* A list of domain name servers to use for the DHCP options set. This parameter is set at creation only; it is not affected by updates. Accepts an array of domain server names. #####`ntp_servers` *Optional* A list of NTP servers to use for the DHCP options set. This parameter is set at creation only; it is not affected by updates. Accepts an array of NTP server names. @@ -644,7 +647,7 @@ The type of customer gateway. The only currently supported value --- and the def #####`name` *Required* The name of the internet gateway. This is the value of the AWS Name tag. - + #####`tags` *Optional* Tags to assign to the internet gateway. Accepts a 'key => value' hash of tags. @@ -668,7 +671,7 @@ The type of customer gateway. The only currently supported value --- and the def #####`routes` *Optional* Individual routes for the routing table. This parameter is set at creation only; it is not affected by updates. Accepts an array of 'destination_cidr_block' and 'gateway' values: - + ~~~ routes => [ { @@ -745,7 +748,7 @@ routes => [ #####`type` *Optional* The type of VPN gateway. This parameter is set at creation only; it is not affected by updates. The only currently supported value --- and the default --- is 'ipsec.1'. -#####`routes` +#####`routes` *Optional* The list of routes for the VPN. This parameter is set at creation only; it is not affected by updates. Valid values are IP ranges like: `routes => ['0.0.0.0/0']` #####`static_routes` @@ -777,11 +780,140 @@ routes => [ #####`type` *Optional* The type of VPN gateway. This parameter is set at creation only; it is not affected by updates. The only currently supported value --- and the default --- is 'ipsec.1'. -#### Type: route53* +#### Type: rds_db_parameter_group + +Note that currently, this type can only be listed via puppet resource, +but cannot be created by Puppet. + +#####`name` +*Required* The name of the parameter group. + +#####`region` +*Required* The region in which to launch the parameter group. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). + +#####`description` +A description of the parameter group. Should be a string. + +#####`family` +The name of the database family with which the parameter group is +compatible; for instance, 'mysql5.1'. + +#### Type: rds_db_securitygroup + +#####`name` +*Required* The name of the RDS DB security group. + +#####`description` +A description of the RDS DB security group. Should be a string. This +parameter is set at creation only; it is not affected by updates. + +#####`region` +*Required* The region in which to launch the parameter group. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). + +#####`owner_id` +The internal AWS id of the owner of the security group. Read-only. + +#####`security_groups` +Details of any EC2 security groups attached to the RDS security group. Read-only. + +#####`ip_ranges` +Details of any ip_ranges attached to the RDS security group and their current state. Read-only. + +#### Type: rds_instance + +#####`name` +*Required* The name of the RDS Instance. + +#####`db_name` +Generally the name of database to be created. For Oracle this is the SID. +Should not be set for MSSQL. + +#####`region` +*Required* The region in which to launch the parameter group. For valid values, see [AWS Regions](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region). + +#####`db_instance_class` +*Required* The size of the database instance. See [the AWS +documentation](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html) +for the list of sizes. + +#####`availability_zone` +*Optional* The availability zone in which to place the instance. For valid availability zone codes, see [AWS Regions and Availability Zones](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). + +#####`engine` +*Required* The type of database to use. Current options can be found +using the `rds-describe-db-engine-versions` command from the AWS CLI. +This parameter is set at creation only; it is not affected by updates. + +#####`engine_version` +The version of the database to use. Current options can be found +using the `rds-describe-db-engine-versions` command from the AWS CLI. +This parameter is set at creation only; it is not affected by updates. + +#####`allocated_storage` +The size of the database in gigabytes. Note that minimum size constraints +exist, which vary depending on the database engine selected. +This parameter is set at creation only; it is not affected by updates. + +#####`license_model` +The nature of the license for commercial database products. Currently +supported values are license-included, bring-your-own-license or +general-public-license. This parameter is set at creation only; it is +not affected by updates. + +#####`storage_type` +The type of storage to back the database with. Currently supported +values are standard, gp2 or io1. This parameter is set at creation only; +it is not affected by updates. + +#####`iops` +The number of provisioned IOPS (input/output operations per second) to +be initially allocated for the instance. This parameter is set at +creation only; it is not affected by updates. + +#####`master_username` +The name of the master user for the database instance. This parameter is +set at creation only; it is not affected by updates. + +#####`master_user_password` +The password for the master user. This parameter is set at creation +only; it is not affected by updates. + +#####`multi_az` +Boolean. Required if you intend to run the instance across multiple +availability zones. This parameter is set at creation only; it is not +affected by updates. + +#####`db_subnet` +The name of an existing DB Subnet, for launching RDS instances in VPC. +This parameter is set at creation only; it is not affected by updates. + +#####`db_security_groups` +Names of the database security groups to associate with the instance. +This parameter is set at creation only; it is not affected by updates. + +#####`endpoint` +The DNS address of the database. Read-only. + +#####`port` +The port that the database is listening on. Read-only. + +#####`skip_final_snapshot` +Determines whether a final DB snapshot is created before the DB instance +is deleted. Defaults to false. + +#####`db_parameter_group` +The name of an associated DB parameter group. Should be a string. This +parameter is set at creation only; it is not affected by updates. + +#####`final_db_snapshot_identifier` +The name of the snapshot created when the instance is terminated. Note +that skip_final_snapshot must be set to false. + +#### Type: route53 The route53 types set up various types of Route53 records: -* `route53_a_record`: Sets up a Route53 DNS record. +* `route53_a_record`: Sets up a Route53 DNS record. * `route53_aaaa_record`: Sets up a Route53 DNS AAAA record. @@ -809,7 +941,7 @@ All Route53 record types use the same parameters: #####`ttl` *Optional* The time to live for the record. Accepts an integer. - + #####`values` *Required* The values of the record. Accepts an array. diff --git a/examples/postgres-rds-example/README.md b/examples/postgres-rds-example/README.md new file mode 100644 index 00000000..5258025a --- /dev/null +++ b/examples/postgres-rds-example/README.md @@ -0,0 +1,54 @@ +# RDS + +[Amazon Relational Database Service](http://aws.amazon.com/rds/) (Amazon RDS) is a web service that makes it easy to set up, operate, and scale a relational database in the cloud. + +## How + +This example creates a security group to allow access to a Postgres RDS instance, then creates that RDS instance with the security group assigned. + + puppet apply rds_security.pp + +Unfortunately, it's not possible to assign the EC2 group and the allowed IPs to the `db_securitygroup` through the API, so you have to do this manually though the console for now: + +## Add the Security Group We Made with Puppet** +![Add EC2 Security Group](./images/add-rds-securitygroup.png?raw=true) + +## Add an IP to allow access to the RDS instance +**Note: Enter `0.0.0.0/32` to allow all IPs** +![Add IP to allow](./images/add-ip-to-allow.png?raw=true) + +## It should look something like this +![Final Look](./images/final-screen.png?raw=true) + +You can now check your security group is correct by using Puppet resource commands: + + puppet resource rds_db_securitygroup rds-postgres-db_securitygroup + +It should return something like this: + +~~~ +rds_db_securitygroup { 'rds-postgres-db_securitygroup': + ensure => 'present', + ec2_security_groups => [{'ec2_security_group_id' => 'sg-83fb3z5', 'ec2_security_group_name' => 'rds-postgres-group', 'ec2_security_group_owner_id' => '4822239859', 'status' => 'authorized'}], + ip_ranges => [{'ip_range' => '0.0.0.0/32', 'status' => 'authorized'}], + owner_id => '239838031', + region => 'us-west-2', +} +~~~ + +When this is complete, create the RDS Postgres instance: + + puppet apply rds_postgres.pp + +This can take a while to setup, but when it's complete, you should be able to access it: + +~~~ +psql -d postgresql -h puppetlabs-aws-postgres.cwgutxb9fmx.us-west-2.rds.amazonaws.com -U root + +Password for user root: pullZstringz345 +psql (9.4.0, server 9.3.5) +SSL connection (protocol: TLSv1.2, cipher: DHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off) +Type "help" for help. + +postgresql=> exit +~~~ diff --git a/examples/postgres-rds-example/images/add-ip-to-allow.png b/examples/postgres-rds-example/images/add-ip-to-allow.png new file mode 100644 index 00000000..53ca49e4 Binary files /dev/null and b/examples/postgres-rds-example/images/add-ip-to-allow.png differ diff --git a/examples/postgres-rds-example/images/add-rds-securitygroup.png b/examples/postgres-rds-example/images/add-rds-securitygroup.png new file mode 100644 index 00000000..55c1164d Binary files /dev/null and b/examples/postgres-rds-example/images/add-rds-securitygroup.png differ diff --git a/examples/postgres-rds-example/images/final-screen.png b/examples/postgres-rds-example/images/final-screen.png new file mode 100644 index 00000000..aa47b4a0 Binary files /dev/null and b/examples/postgres-rds-example/images/final-screen.png differ diff --git a/examples/postgres-rds-example/rds_postgres.pp b/examples/postgres-rds-example/rds_postgres.pp new file mode 100644 index 00000000..3d2997f0 --- /dev/null +++ b/examples/postgres-rds-example/rds_postgres.pp @@ -0,0 +1,14 @@ +rds_instance { 'puppetlabs-aws-postgres': + ensure => present, + allocated_storage => '5', + db_instance_class => 'db.m3.medium', + db_name => 'postgresql', + engine => 'postgres', + license_model => 'postgresql-license', + db_security_groups => 'rds-postgres-db_securitygroup', + master_username => 'root', + master_user_password=> 'pullZstringz345', + region => 'us-west-2', + skip_final_snapshot => 'true', + storage_type => 'gp2', +} diff --git a/examples/postgres-rds-example/rds_security.pp b/examples/postgres-rds-example/rds_security.pp new file mode 100644 index 00000000..715b59f0 --- /dev/null +++ b/examples/postgres-rds-example/rds_security.pp @@ -0,0 +1,18 @@ +ec2_securitygroup { 'rds-postgres-group': + ensure => present, + region => 'us-west-2', + description => 'Group for Allowing access to Postgres (Port 5432)', + ingress => [{ + security_group => 'rds-postgres-group', + },{ + protocol => 'tcp', + port => 5432, + cidr => '0.0.0.0/0', + }] +} + +rds_db_securitygroup { 'rds-postgres-db_securitygroup': + ensure => present, + region => 'us-west-2', + description => 'An RDS Security group to allow Postgres', +} diff --git a/lib/puppet/provider/rds_db_parameter_group/v2.rb b/lib/puppet/provider/rds_db_parameter_group/v2.rb new file mode 100644 index 00000000..f46cfc38 --- /dev/null +++ b/lib/puppet/provider/rds_db_parameter_group/v2.rb @@ -0,0 +1,37 @@ +require_relative '../../../puppet_x/puppetlabs/aws.rb' + +Puppet::Type.type(:rds_db_parameter_group).provide(:v2, :parent => PuppetX::Puppetlabs::Aws) do + confine feature: :aws + + mk_resource_methods + + def self.instances + regions.collect do |region| + instances = [] + rds_client(region).describe_db_parameter_groups.each do |response| + response.data.db_parameter_groups.each do |db_parameter_group| + # There's always a default class + hash = db_parameter_group_to_hash(region, db_parameter_group) + instances << new(hash) if hash[:name] + end + end + instances + end.flatten + end + + def self.db_parameter_group_to_hash(region, db_parameter_group) + { + :ensure => :present, + :name => db_parameter_group.db_parameter_group_name, + :description => db_parameter_group.description, + :family => db_parameter_group.db_parameter_group_family, + :region => region, + } + end + + def exists? + Puppet.info("Checking if DB Parameter Group #{name} exists") + [:present, :creating, :available].include? @property_hash[:ensure] + end + +end \ No newline at end of file diff --git a/lib/puppet/provider/rds_db_securitygroup/v2.rb b/lib/puppet/provider/rds_db_securitygroup/v2.rb new file mode 100644 index 00000000..a96aadd3 --- /dev/null +++ b/lib/puppet/provider/rds_db_securitygroup/v2.rb @@ -0,0 +1,93 @@ +require_relative '../../../puppet_x/puppetlabs/aws.rb' + +Puppet::Type.type(:rds_db_securitygroup).provide(:v2, :parent => PuppetX::Puppetlabs::Aws) do + confine feature: :aws + + mk_resource_methods + + def self.instances + regions.collect do |region| + instances = [] + rds_client(region).describe_db_security_groups.each do |response| + response.data.db_security_groups.each do |db_security_group| + # There's always a default class + unless db_security_group.db_security_group_name =~ /^default$/ + hash = db_security_group_to_hash(region, db_security_group) + instances << new(hash) if hash[:name] + end + end + end + instances + end.flatten + end + + def self.prefetch(resources) + instances.each do |prov| + if resource = resources[prov.name] # rubocop:disable Lint/AssignmentInCondition + resource.provider = prov if resource[:region] == prov.region + end + end + end + + read_only(:region, :description) + + def self.db_security_group_to_hash(region, db_security_group) + { + :ensure => :present, + :region => region, + :name => db_security_group.db_security_group_name, + :description => db_security_group.db_security_group_description, + :owner_id => db_security_group.owner_id, + :security_groups => ec2_security_group_to_array_of_hashes(db_security_group.ec2_security_groups), + :ip_ranges => ip_ranges_to_array_of_hashes(db_security_group.ip_ranges), + } + end + + def exists? + Puppet.info("Checking if DB Security Group #{name} exists") + [:present, :creating, :available].include? @property_hash[:ensure] + end + + def create + Puppet.info("Creating DB Security Group #{name}") + config = { + :db_security_group_name => resource[:name], + :db_security_group_description => resource[:description], + } + + rds_client(resource[:region]).create_db_security_group(config) + + @property_hash[:ensure] = :present + end + + def destroy + Puppet.info("Deleting DB Security Group #{name} in region #{resource[:region]}") + rds = rds_client(resource[:region]) + config = { + db_security_group_name: name, + } + rds.delete_db_security_group(config) + @property_hash[:ensure] = :absent + end + + def self.ec2_security_group_to_array_of_hashes(ec2_security_groups) + ec2_security_groups.collect do |group| + { + :status => group.status, + :ec2_security_group_name => group.ec2_security_group_name, + :ec2_security_group_owner_id => group.ec2_security_group_owner_id, + :ec2_security_group_id => group.ec2_security_group_id, + } + end + end + + def self.ip_ranges_to_array_of_hashes(ip_ranges) + ip_ranges.collect do |group| + { + :status => group.status, + :ip_range => group.cidrip, + } + end + end + +end diff --git a/lib/puppet/provider/rds_instance/v2.rb b/lib/puppet/provider/rds_instance/v2.rb new file mode 100644 index 00000000..6ffc4b31 --- /dev/null +++ b/lib/puppet/provider/rds_instance/v2.rb @@ -0,0 +1,107 @@ +require_relative '../../../puppet_x/puppetlabs/aws.rb' + +Puppet::Type.type(:rds_instance).provide(:v2, :parent => PuppetX::Puppetlabs::Aws) do + confine feature: :aws + + mk_resource_methods + + def self.instances + regions.collect do |region| + instances = [] + rds_client(region).describe_db_instances.each do |response| + response.data.db_instances.each do |db| + unless db.db_instance_status =~ /^deleted$|^deleting$/ + hash = db_instance_to_hash(region, db) + instances << new(hash) if hash[:name] + end + end + end + instances + end.flatten + end + + read_only(:iops, :master_username, :multi_az, :license_model, + :db_name, :region, :db_instance_class, :availability_zone, + :engine, :engine_version, :allocated_storage, :storage_type, + :db_security_groups, :db_parameter_group) + + def self.prefetch(resources) + instances.each do |prov| + if resource = resources[prov.name] # rubocop:disable Lint/AssignmentInCondition + resource.provider = prov if resource[:region] == prov.region + end + end + end + + def self.db_instance_to_hash(region, instance) + config = { + ensure: :present, + name: instance.db_instance_identifier, + region: region, + engine: instance.engine, + engine_version: instance.engine_version, + db_instance_class: instance.db_instance_class, + master_username: instance.master_username, + db_name: instance.db_name, + allocated_storage: instance.allocated_storage, + storage_type: instance.storage_type, + license_model: instance.license_model, + multi_az: instance.multi_az, + iops: instance.iops, + db_parameter_group: instance.db_parameter_groups.collect(&:db_parameter_group_name).first, + db_security_groups: instance.db_security_groups.collect(&:db_security_group_name), + } + if instance.respond_to?('endpoint') && !instance.endpoint.nil? + config[:endpoint] = instance.endpoint.address + config[:port] = instance.endpoint.port + end + config + end + + def exists? + dest_region = resource[:region] if resource + Puppet.info("Checking if instance #{name} exists in region #{dest_region || region}") + [:present, :creating, :available, :backing_up].include? @property_hash[:ensure] + end + + def create + Puppet.info("Starting DB instance #{name}") + config = { + db_instance_identifier: resource[:name], + db_name: resource[:db_name], + db_instance_class: resource[:db_instance_class], + engine: resource[:engine], + engine_version: resource[:engine_version], + license_model: resource[:license_model], + storage_type: resource[:storage_type], + multi_az: resource[:multi_az].to_s, + allocated_storage: resource[:allocated_storage], + iops: resource[:iops], + master_username: resource[:master_username], + master_user_password: resource[:master_user_password], + subnet_group_name: resource[:db_subnet], + db_security_groups: resource[:db_security_groups], + db_parameter_group_name: resource[:db_parameter_group], + } + + rds_client(resource[:region]).create_db_instance(config) + + @property_hash[:ensure] = :present + end + + def destroy + Puppet.info("Deleting database #{name} in region #{resource[:region]}") + rds = rds_client(resource[:region]) + if resource[:skip_final_snapshot].to_s == 'true' + Puppet.info("A snapshot of the database on deletion will be available as #{resource[:final_db_snapshot_identifier]}") + end + config = { + db_instance_identifier: name, + skip_final_snapshot: resource[:skip_final_snapshot].to_s, + final_db_snapshot_identifier: resource[:final_db_snapshot_identifier], + } + rds.delete_db_instance(config) + @property_hash[:ensure] = :absent + end + +end diff --git a/lib/puppet/type/rds_db_parameter_group.rb b/lib/puppet/type/rds_db_parameter_group.rb new file mode 100644 index 00000000..5c36d7a8 --- /dev/null +++ b/lib/puppet/type/rds_db_parameter_group.rb @@ -0,0 +1,35 @@ +Puppet::Type.newtype(:rds_db_parameter_group) do + @doc = 'Type representing an RDS DB Parameter group.' + + ensurable + + newparam(:name, namevar: true) do + desc 'The name of the DB Parameter Group (also known as the db_parameter_group_name).' + validate do |value| + fail 'name should be a String' unless value.is_a?(String) + end + end + + newproperty(:description) do + desc 'The description of a DB parameter group.' + validate do |value| + fail 'description should be a String' unless value.is_a?(String) + end + end + + newproperty(:family) do + desc 'The name of the DB family that this DB parameter group is compatible with (eg. mysql5.1).' + validate do |value| + fail 'family should be a String' unless value.is_a?(String) + end + end + + newproperty(:region) do + desc 'The region in which to create the db_parameter_group.' + validate do |value| + fail 'region should be a String' unless value.is_a?(String) + fail 'region should not contain spaces' if value =~ /\s/ + end + end + +end diff --git a/lib/puppet/type/rds_db_securitygroup.rb b/lib/puppet/type/rds_db_securitygroup.rb new file mode 100644 index 00000000..c30d4a67 --- /dev/null +++ b/lib/puppet/type/rds_db_securitygroup.rb @@ -0,0 +1,55 @@ +Puppet::Type.newtype(:rds_db_securitygroup) do + @doc = 'Type representing an RDS instance.' + + ensurable + + newparam(:name, namevar: true) do + desc 'The name of the DB Security Group (also known as the db_security_group_name).' + validate do |value| + fail 'name should be a String' unless value.is_a?(String) + end + end + + newparam(:description) do + desc 'The description of a DB Security group.' + validate do |value| + fail 'description should be a String' unless value.is_a?(String) + fail 'description should not be blank' if value == '' + end + end + + newproperty(:owner_id) do + desc 'The ID of the owner of this DB Security Group.' + validate do |value| + fail 'owner_id is read-only' + end + end + + newproperty(:security_groups, :array_matching => :all) do + desc 'The EC2 Security Groups assigned to this RDS DB security group.' + validate do |value| + fail 'security_groups is read-only' + end + end + + newproperty(:region) do + desc 'The region in which to create the db_securitygroup.' + validate do |value| + fail 'region should be a String' unless value.is_a?(String) + fail 'region should not contain spaces' if value =~ /\s/ + end + end + + newproperty(:ip_ranges, :array_matching => :all) do + desc 'The IP ranges allowed to access the RDS instance.' + validate do |value| + fail 'ip_ranges is read-only' + end + end + + autorequire(:ec2_securitygroup) do + groups = self[:security_groups] + groups.is_a?(Array) ? groups : [groups] + end + +end diff --git a/lib/puppet/type/rds_instance.rb b/lib/puppet/type/rds_instance.rb new file mode 100644 index 00000000..b8416c8f --- /dev/null +++ b/lib/puppet/type/rds_instance.rb @@ -0,0 +1,202 @@ +Puppet::Type.newtype(:rds_instance) do + @doc = 'Type representing an RDS instance.' + + ensurable + + newparam(:name, namevar: true) do + desc 'The name of the db instance (also known as the db_instance_identifier)' + validate do |value| + fail 'name should be a String' unless value.is_a?(String) + fail 'RDS Instances must have a name' if value == '' + end + end + + newproperty(:db_name) do + desc 'The meaning of this parameter differs according to the database engine you use. +Type: String + +MySQL + +The name of the database to create when the DB instance is created. If this parameter is not specified, no database is created in the DB instance. + +Constraints: + +Must contain 1 to 64 alphanumeric characters +Cannot be a word reserved by the specified database engine + +PostgreSQL + +The name of the database to create when the DB instance is created. If this parameter is not specified, no database is created in the DB instance. + +Constraints: + +Must contain 1 to 63 alphanumeric characters +Must begin with a letter or an underscore. Subsequent characters can be letters, underscores, or digits (0-9). +Cannot be a word reserved by the specified database engine +Oracle + +The Oracle System ID (SID) of the created DB instance. + +Default: ORCL + +Constraints: + +Cannot be longer than 8 characters +SQL Server + +Not applicable. Must be null.' + end + + newproperty(:region) do + desc 'The region in which to launch the instance.' + validate do |value| + fail 'region should be a String' unless value.is_a?(String) + fail 'region should not contain spaces' if value =~ /\s/ + end + end + + newproperty(:db_instance_class) do + desc 'The instance class to use for the instance eg. db.m3.medium.' + validate do |value| + fail 'db_instance_class should be a String' unless value.is_a?(String) + fail 'db_instance_class should not contain spaces' if value =~ /\s/ + fail 'db_instance_class should not be blank' if value == '' + end + end + + newproperty(:availability_zone) do + desc 'The availability zone in which to place the instance.' + validate do |value| + fail 'availability_zone should be a String' unless value.is_a?(String) + fail 'availability_zone should not contain spaces' if value =~ /\s/ + fail 'availability_zone should not be blank' if value == '' + end + end + + newproperty(:engine) do + desc 'the type of Database for the instance( mysql, postgres, etc...)' + validate do |value| + fail 'engine should be a String' unless value.is_a?(String) + fail 'engine type should not contains spaces' if value =~ /\s/ + fail 'engine should not be blank' if value == '' + end + end + + newproperty(:engine_version) do + desc 'The version of Database for the instance.' + validate do |value| + fail 'engine_version should be a String' unless value.is_a?(String) + fail 'engine_version type should not contains spaces' if value =~ /\s/ + fail 'engine_version should not be blank' if value == '' + end + end + + newproperty(:allocated_storage) do + desc 'The size of the DB.' + validate do |value| + fail 'allocated_storage type should not contains spaces' if value =~ /\s/ + fail 'allocated_storage should not be blank' if value == '' + end + end + + newproperty(:license_model) do + desc 'The license for the instance (Valid values: license-included | bring-your-own-license | general-public-license).' + validate do |value| + fail 'license_model should be a String' unless value.is_a?(String) + fail 'license_model type should not contains spaces' if value =~ /\s/ + end + end + + newproperty(:storage_type) do + desc 'The storage type for the DB (Valid values: gp | io1 *Note: If you specify io1, you must also include a value for the Iops parameter).' + validate do |value| + fail 'storage_type should be a String' unless value.is_a?(String) + fail 'storage_type should not contains spaces' if value =~ /\s/ + end + end + + newproperty(:iops) do + desc 'The number of input/output operations per second for the database.' + validate do |value| + fail 'IOPS must be an integer' unless value =~ /^\d+$/ + end + end + + newproperty(:master_username) do + desc 'The main user for the DB.' + validate do |value| + fail 'master_username should be a String' unless value.is_a?(String) + fail 'master_username type should not contains spaces' if value =~ /\s/ + end + end + + newparam(:master_user_password) do + desc 'The main user Password.' + validate do |value| + fail 'master_user_password should be a String' unless value.is_a?(String) + fail 'master_user_password should not be blank' if value == '' + end + end + + newproperty(:multi_az) do + desc 'Define a multi-az.' + defaultto :false + newvalues(:false, :'false', :'true') + def insync?(is) + is.to_s == should.to_s + end + end + + newparam(:db_subnet) do + desc 'The VPC DB subnet for this instance.' + validate do |value| + fail 'subnet_group_name should be a String' unless value.is_a?(String) + fail 'subnet_group_name should not be blank' if value == '' + end + end + + newproperty(:db_security_groups, :array_matching => :all) do + desc 'The DB security groups to assign to this RDS instance.' + end + + newproperty(:endpoint) do + desc 'The connection endpoint for the database.' + validate do |value| + fail 'endpoint is read-only' + end + end + + newproperty(:port) do + desc 'The port the database is running on.' + validate do |value| + fail 'port is read-only' + end + end + + newparam(:skip_final_snapshot) do + desc 'Skip snapshot on deletion.' + defaultto :false + newvalues(:false, :'false', :'true') + end + + newproperty(:db_parameter_group) do + desc 'The DB parameter group for this RDS instance.' + validate do |value| + fail 'db_parameter_group should be a String' unless value.is_a?(String) + end + end + + newparam(:final_db_snapshot_identifier) do + desc 'Name given to the last snapshot on deletion.' + validate do |value| + fail 'final_db_snapshot_identifier should be a String' unless value.is_a?(String) + fail 'final_db_snapshot_identifier should not be blank' if value == '' + end + end + + autorequire(:rds_db_securitygroup) do + groups = self[:db_security_groups] + groups.is_a?(Array) ? groups : [groups] + end + +end diff --git a/lib/puppet_x/puppetlabs/aws.rb b/lib/puppet_x/puppetlabs/aws.rb index b380b73e..ea0caaac 100644 --- a/lib/puppet_x/puppetlabs/aws.rb +++ b/lib/puppet_x/puppetlabs/aws.rb @@ -120,6 +120,14 @@ def route53_client(region = default_region) self.class.route53_client(region) end + def rds_client(region = default_region) + self.class.rds_client(region) + end + + def self.rds_client(region = default_region) + ::Aws::RDS::Client.new({region: region}) + end + def tags_for_resource tags = resource[:tags] ? resource[:tags].map { |k,v| {key: k, value: v} } : [] tags << {key: 'Name', value: name} diff --git a/spec/acceptance/fixtures/rds.pp.tmpl b/spec/acceptance/fixtures/rds.pp.tmpl new file mode 100644 index 00000000..0d0ec755 --- /dev/null +++ b/spec/acceptance/fixtures/rds.pp.tmpl @@ -0,0 +1,15 @@ +rds_instance { '{{name}}': + ensure => '{{ensure}}', + region => '{{region}}', + db_name => '{{db_name}}', + engine => '{{engine}}', + allocated_storage => '{{allocated_storage}}', + engine_version => '{{engine_version}}', + license_model => '{{license_model}}', + storage_type => '{{storage_type}}', + db_instance_class => '{{db_instance_class}}', + master_username => '{{master_username}}', + master_user_password => '{{master_user_password}}', + multi_az => '{{multi_az}}', + skip_final_snapshot => '{{skip_final_snapshot}}', +} diff --git a/spec/acceptance/fixtures/rds_db_securitygroup.pp.tmpl b/spec/acceptance/fixtures/rds_db_securitygroup.pp.tmpl new file mode 100644 index 00000000..081bceb7 --- /dev/null +++ b/spec/acceptance/fixtures/rds_db_securitygroup.pp.tmpl @@ -0,0 +1,5 @@ +rds_db_securitygroup { '{{name}}': + ensure => '{{ensure}}', + region => '{{region}}', + description => '{{description}}', +} diff --git a/spec/acceptance/rds_db_securitygroup_spec.rb b/spec/acceptance/rds_db_securitygroup_spec.rb new file mode 100644 index 00000000..d828f183 --- /dev/null +++ b/spec/acceptance/rds_db_securitygroup_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper_acceptance' +require 'securerandom' + +describe "rds_db_securitygroup" do + + before(:all) do + @default_region = 'sa-east-1' + @aws = AwsHelper.new(@default_region) + @template = 'rds_db_securitygroup.pp.tmpl' + end + + def get_db_securitygroup(name) + db_security_groups = @aws.get_db_security_groups(name) + expect(db_security_groups.count).to eq(1) + db_security_groups.first + end + + describe 'should create a new group' do + + before(:all) do + @config = { + :name => "#{PuppetManifest.rds_id}-#{SecureRandom.hex}", + :ensure => 'present', + :region => @default_region, + :description => 'Acceptance test', + } + + manifest = PuppetManifest.new(@template, @config) + manifest.apply + @db_securitygroup = get_db_securitygroup(@config[:name]) + end + + after(:all) do + new_config = @config.update({:ensure => 'absent'}) + PuppetManifest.new(@template, new_config).apply + end + + it 'with the specified name' do + expect(@db_securitygroup.db_security_group_name).to eq(@config[:name]) + end + + it 'with the specified description' do + expect(@db_securitygroup.db_security_group_description).to eq(@config[:description]) + end + + it 'with no associated ip ranges' do + expect(@db_securitygroup.ip_ranges).to be_empty + end + + it 'with no associated security groups' do + expect(@db_securitygroup.ec2_security_groups).to be_empty + end + + end +end diff --git a/spec/acceptance/rds_spec.rb b/spec/acceptance/rds_spec.rb new file mode 100644 index 00000000..982b427e --- /dev/null +++ b/spec/acceptance/rds_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper_acceptance' +require 'securerandom' + +describe "rds_instance" do + + before(:all) do + @default_region = 'sa-east-1' + @aws = AwsHelper.new(@default_region) + @template = 'rds.pp.tmpl' + end + + def get_rds_instance(name) + rds_instances = @aws.get_rds_instance(name) + expect(rds_instances.count).to eq(1) + rds_instances.first + end + + describe 'should create a new database' do + + before(:all) do + @config = { + :name => "#{PuppetManifest.rds_id}-#{SecureRandom.hex}", + :ensure => 'present', + :region => @default_region, + :db_name => 'puppet', + :engine => 'mysql', + :allocated_storage => 5, + :engine_version => '5.6.19a', + :license_model => 'general-public-license', + :storage_type => 'gp2', + :db_instance_class => 'db.m3.medium', + :master_username => 'puppet', + :master_user_password => 'pullth3stringz', + :multi_az => false, + :skip_final_snapshot => true, + } + + manifest = PuppetManifest.new(@template, @config) + manifest.apply + @rds_instance = get_rds_instance(@config[:name]) + end + + after(:all) do + new_config = @config.update({:ensure => 'absent'}) + PuppetManifest.new(@template, new_config).apply + end + + it 'with the specified name' do + expect(@rds_instance.db_instance_identifier).to eq(@config[:name]) + end + + it 'with the specified db_name' do + expect(@rds_instance.db_name).to eq(@config[:db_name]) + end + + it 'with the specified engine' do + expect(@rds_instance.engine).to eq(@config[:engine]) + end + + it 'with the specified license' do + expect(@rds_instance.license_model).to eq(@config[:license_model]) + end + + it 'with the specified engine version' do + expect(@rds_instance.engine_version).to eq(@config[:engine_version]) + end + + it 'with the specified db name' do + expect(@rds_instance.db_name).to eq(@config[:db_name]) + end + + it 'with the specified username' do + expect(@rds_instance.master_username).to eq(@config[:master_username]) + end + + it 'with the specified storage' do + expect(@rds_instance.allocated_storage).to eq(@config[:allocated_storage]) + end + + it 'with the specified instance class' do + expect(@rds_instance.db_instance_class).to eq(@config[:db_instance_class]) + end + + it 'with the specified storage type' do + expect(@rds_instance.storage_type).to eq(@config[:storage_type]) + end + + it 'not associated with a VPC (EC2-Classic accounts only)' do + unless @aws.vpc_only? + expect(@rds_instance.vpc_security_groups).to be_empty + expect(@rds_instance.db_subnet_group).to be_nil + end + end + + context 'when viewing the database via puppet resource' do + + before(:all) do + @result = TestExecutor.puppet_resource('rds_instance', {:name => @config[:name]}, '--modulepath ../') + end + + it 'ensure is correct' do + regex = /(ensure)(\s*)(=>)(\s*)('present')/ + expect(@result.stdout).to match(regex) + end + + it 'allocated storage is correct' do + regex = /(allocated_storage)(\s*)(=>)(\s*)('#{@config[:allocated_storage]}')/ + expect(@result.stdout).to match(regex) + end + + it 'db instance class is correct' do + regex = /(db_instance_class)(\s*)(=>)(\s*)('#{@config[:db_instance_class]}')/ + expect(@result.stdout).to match(regex) + end + + it 'engine is correct' do + regex = /(engine)(\s*)(=>)(\s*)('#{@config[:engine]}')/ + expect(@result.stdout).to match(regex) + end + + it 'license model is correct' do + regex = /(license_model)(\s*)(=>)(\s*)('#{@config[:license_model]}')/ + expect(@result.stdout).to match(regex) + end + + it 'master username is correct' do + regex = /(master_username)(\s*)(=>)(\s*)('#{@config[:master_username]}')/ + expect(@result.stdout).to match(regex) + end + + it 'region is correct' do + regex = /(region)(\s*)(=>)(\s*)('#{@config[:region]}')/ + expect(@result.stdout).to match(regex) + end + + it 'storage type is correct' do + regex = /(storage_type)(\s*)(=>)(\s*)('#{@config[:storage_type]}')/ + expect(@result.stdout).to match(regex) + end + end + + end +end diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb index 6eba60bf..2789057d 100644 --- a/spec/spec_helper_acceptance.rb +++ b/spec/spec_helper_acceptance.rb @@ -15,7 +15,7 @@ def initialize(file, config) def apply manifest = self.render.gsub("\n", '') - cmd = "bundle exec puppet apply --detailed-exitcodes -e \"#{manifest}\" --modulepath ../" + cmd = "bundle exec puppet apply --detailed-exitcodes -e \"#{manifest}\" --modulepath ../ --trace" result = { output: [], exit_status: nil } Open3.popen2e(cmd) do |stdin, stdout_err, wait_thr| @@ -69,6 +69,13 @@ def self.env_id ).gsub(/'/, '') end + def self.rds_id + @rds_id ||= ( + ENV['BUILD_DISPLAY_NAME'] || + (ENV['USER']) + ).gsub(/'/, '') + end + def self.env_dns_id @env_dns_id ||= @env_id.gsub(/[^\\dA-Za-z-]/, '') end @@ -83,6 +90,21 @@ def initialize(region) @autoscaling_client = ::Aws::AutoScaling::Client.new({region: region}) @cloudwatch_client = ::Aws::CloudWatch::Client.new({region: region}) @route53_client = ::Aws::Route53::Client.new({region: region}) + @rds_client = ::Aws::RDS::Client.new({region: region}) + end + + def get_rds_instance(name) + response = @rds_client.describe_db_instances( + db_instance_identifier: name + ) + response.data.db_instances + end + + def get_db_security_groups(name) + response = @rds_client.describe_db_security_groups( + db_security_group_name: name + ) + response.data.db_security_groups end def get_instances(name) diff --git a/spec/unit/provider/rds_db_securitygroup/v2_spec.rb b/spec/unit/provider/rds_db_securitygroup/v2_spec.rb new file mode 100644 index 00000000..5e72fa30 --- /dev/null +++ b/spec/unit/provider/rds_db_securitygroup/v2_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +provider_class = Puppet::Type.type(:rds_db_securitygroup).provider(:v2) + +ENV['AWS_ACCESS_KEY_ID'] = 'redacted' +ENV['AWS_SECRET_ACCESS_KEY'] = 'redacted' +ENV['AWS_REGION'] = 'sa-east-1' + +describe provider_class do + + let(:resource) { + Puppet::Type.type(:rds_db_securitygroup).new( + :name => "awesome-db_securitygroup", + :ensure => 'present', + :description => 'DB Security Group', + ) + } + + let(:provider) { resource.provider } + + let(:instance) { provider.class.instances.first } + + it 'should be an instance of the ProviderV2' do + expect(provider).to be_an_instance_of Puppet::Type::Rds_db_securitygroup::ProviderV2 + end + +end diff --git a/spec/unit/provider/rds_instance/v2_spec.rb b/spec/unit/provider/rds_instance/v2_spec.rb new file mode 100644 index 00000000..5a27c6e1 --- /dev/null +++ b/spec/unit/provider/rds_instance/v2_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +provider_class = Puppet::Type.type(:rds_instance).provider(:v2) + +ENV['AWS_ACCESS_KEY_ID'] = 'redacted' +ENV['AWS_SECRET_ACCESS_KEY'] = 'redacted' +ENV['AWS_REGION'] = 'sa-east-1' + +describe provider_class do + + let(:resource) { + Puppet::Type.type(:rds_instance).new( + ensure: 'present', + name: 'awesome-db-5', + region: 'us-west-1', + db_name: 'mysqldbname3', + engine: 'mysql', + engine_version: '5.6.19a', + license_model: 'general-public-license', + allocated_storage: 10, + availability_zone: 'us-west-1a', + storage_type: 'gp2', + db_instance_class: 'db.m3.medium', + master_username: 'awsusername', + master_user_password: 'the-master-password', + multi_az: false, + ) + } + + let(:provider) { resource.provider } + + let(:instance) { provider.class.instances.first } + + it 'should be an instance of the ProviderV2' do + expect(provider).to be_an_instance_of Puppet::Type::Rds_instance::ProviderV2 + end + +end \ No newline at end of file diff --git a/spec/unit/type/rds_db_parameter_group_spec.rb b/spec/unit/type/rds_db_parameter_group_spec.rb new file mode 100644 index 00000000..2bc2d1c2 --- /dev/null +++ b/spec/unit/type/rds_db_parameter_group_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +type_class = Puppet::Type.type(:rds_db_parameter_group) + +describe type_class do + + let :params do + [ + :name, + ] + end + + let :properties do + [ + :description, + :family, + :region, + ] + end + + it 'should have expected properties' do + properties.each do |property| + expect(type_class.properties.map(&:name)).to be_include(property) + end + end + + it 'should have expected parameters' do + params.each do |param| + expect(type_class.parameters).to be_include(param) + end + end + + it 'should require a name' do + expect { + type_class.new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + + it 'should require a valid looking region' do + expect { + type_class.new({:name => 'sample', :region => 'definitely invalid'}) + }.to raise_error(Puppet::Error, /region should not contain spaces/) + end + + [ + 'name', + 'description', + 'family', + 'region', + ].each do |property| + it "should require #{property} to be a string" do + expect(type_class).to require_string_for(property) + end + end + +end diff --git a/spec/unit/type/rds_db_securitygroup_spec.rb b/spec/unit/type/rds_db_securitygroup_spec.rb new file mode 100644 index 00000000..8d830a54 --- /dev/null +++ b/spec/unit/type/rds_db_securitygroup_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +type_class = Puppet::Type.type(:rds_db_securitygroup) + +describe type_class do + + let :params do + [ + :name, + :description, + ] + end + + let :properties do + [ + :owner_id, + :security_groups, + :region, + :ip_ranges, + ] + end + + it 'should have expected properties' do + properties.each do |property| + expect(type_class.properties.map(&:name)).to be_include(property) + end + end + + it 'should have expected parameters' do + params.each do |param| + expect(type_class.parameters).to be_include(param) + end + end + + it 'should require a name' do + expect { + type_class.new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + + it 'should require a value for description' do + expect { + type_class.new({:name => 'sample', :description => ''}) + }.to raise_error(Puppet::Error, /description should not be blank/) + end + + it 'should require a valid looking region' do + expect { + type_class.new({:name => 'sample', :region => 'definitely invalid'}) + }.to raise_error(Puppet::Error, /region should not contain spaces/) + end + + [ + 'name', + 'region', + 'description', + ].each do |property| + it "should require #{property} to be a string" do + expect(type_class).to require_string_for(property) + end + end + + [ + :security_groups, + :ip_ranges, + :owner_id, + ].each do |property| + it "should have a read-only property of #{property}" do + expect { + config = {:name => 'sample'} + config[property] = 'present' + type_class.new(config) + }.to raise_error(Puppet::Error, /#{property} is read-only/) + end + end +end diff --git a/spec/unit/type/rds_instance_spec.rb b/spec/unit/type/rds_instance_spec.rb new file mode 100644 index 00000000..946577a2 --- /dev/null +++ b/spec/unit/type/rds_instance_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +type_class = Puppet::Type.type(:rds_instance) + +describe type_class do + + let :params do + [ + :name, + :db_subnet, + :master_user_password, + :skip_final_snapshot, + :final_db_snapshot_identifier, + ] + end + + let :properties do + [ + :ensure, + :region, + :db_name, + :db_instance_class, + :availability_zone, + :engine, + :engine_version, + :allocated_storage, + :license_model, + :storage_type, + :iops, + :master_username, + :multi_az, + :db_security_groups, + :endpoint, + :port, + :db_parameter_group, + ] + end + + it 'should have expected properties' do + properties.each do |property| + expect(type_class.properties.map(&:name)).to be_include(property) + end + end + + it 'should have expected parameters' do + params.each do |param| + expect(type_class.parameters).to be_include(param) + end + end + + it 'should require a name' do + expect { + type_class.new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + + it 'region should not contain spaces' do + expect { + type_class.new(:name => 'sample', :region => 'sa east 1') + }.to raise_error(Puppet::ResourceError, /region should not contain spaces/) + end + + it 'IOPS must be an integer' do + expect { + type_class.new(:name => 'sample', :iops => 'Ten') + }.to raise_error(Puppet::ResourceError, /IOPS must be an integer/) + end + + it 'should default skip_final_snapshot to false' do + srv = type_class.new(:name => 'sample') + expect(srv[:skip_final_snapshot]).to eq(:false) + end + + it 'should default mult_az to false' do + srv = type_class.new(:name => 'sample') + expect(srv[:multi_az]).to eq(:false) + end + + [ + 'name', + 'region', + 'db_instance_class', + 'availability_zone', + 'engine', + 'engine_version', + 'license_model', + 'storage_type', + 'master_username', + 'master_user_password', + :db_parameter_group, + 'final_db_snapshot_identifier', + ].each do |property| + it "should require #{property} to be a string" do + expect(type_class).to require_string_for(property) + end + end + + [ + :endpoint, + :port, + ].each do |property| + it "should have a read-only property of #{property}" do + expect { + config = {:name => 'sample'} + config[property] = 'present' + type_class.new(config) + }.to raise_error(Puppet::Error, /#{property} is read-only/) + end + end + +end