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

aws_rds_instance - update to support modify, and wait for create #523

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
154 changes: 127 additions & 27 deletions lib/chef/provider/aws_rds_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,147 @@ class Chef::Provider::AwsRdsInstance < Chef::Provisioning::AWSDriver::AWSProvide

provides :aws_rds_instance

## any new first class attributes that should be passed to rds should be added here. these are used to assemble options_hash
REQUIRED_OPTIONS = %i(db_instance_identifier allocated_storage engine
db_instance_class master_username master_user_password)

OTHER_OPTIONS = %i(engine_version multi_az iops publicly_accessible db_name port db_subnet_group_name db_parameter_group_name)


## update (and therefor modify) will ALWAYS called on any run after a create
## there's no sane ability to compare desired state vs current state without extensive per-option logic
## calling modify (even with/without apply_immediately) is safe - it only
## "updates" the master password (modify has know way to determine the previous
## one, of course), which is effectively a non-op.
def update_aws_object(instance)
Chef::Log.warn("aws_rds_instance does not support modifying a started instance")
# There are required optiosn (like `allocated_storage`) that the use may not
# specify on a resource to perform an update. For example, they may want to
# only specify iops to modify that attribute on an update after initial
# creation. In this case we need to load the required options from the existing
# aws_object and only override it if the user has specified a value in the
# resource. Ideally, it would be nice to mark values as required on the
# resource but right now there is not a `required_on_create`. This would
# also be different if chef-provisioning performed resource cloning, which
# it does not.
end

# TODO
### these options need to be transformed...this could get hairy?
### create and modify use different names for them.
### and re-naming an instance could definitely get weird.
# db_instance_identifier - create
# new_db_instance_identifier - modify
# port - create
# db_port_number - modify

## remove create specific options we can't pass to modify
[:engine, :master_username, :db_subnet_group_name, :availability_zone, :character_set_name, :db_cluster_identifier, :db_name, :kms_key_id, :storage_encrypted, :tags, :timezone].each do |key|
options_hash.delete(key)
end

## always wait for a safe state (available) before we try to apply a modification.
wait_for(
aws_object: instance,
query_method: :db_instance_status,
expected_responses: ['available'],
tries: new_resource.wait_tries,
sleep: new_resource.wait_time
) { |instance|
instance.reload
Chef::Log.info "Update RDS instance: before update, waiting for #{new_resource.db_instance_identifier} to be available. State: #{instance.db_instance_status} - pending: #{instance.pending_modified_values.to_h}" if instance.db_instance_status != "available"
}

updated={} #so we can use this outside the converge_by
converge_by "update RDS instance #{new_resource.db_instance_identifier} in #{region}" do
updated=new_resource.driver.rds_client.modify_db_instance(options_hash).to_h[:db_instance]
end

if new_resource.wait_for_update
slept=false
## use the response from modify to determine if we applied an update we should wait for
updated[:pending_modified_values].each do |k, v|
## we ALWAYS apply an update, but we dont need to "wait" for the master_user_password (or do we?)
if k.to_s != "master_user_password"
if ! slept #maybe we should just break the loop?
Chef::Log.info "Updated RDS instance: #{new_resource.db_instance_identifier}, sleeping #{new_resource.wait_time} seconds to verify state is now available due to #{updated[:pending_modified_values]}"
sleep new_resource.wait_time #it takes a few seconds before the instance goes out of 'available'
slept=true
end
converge_by "waiting until RDS instance is available after update #{new_resource.db_instance_identifier} in #{region}" do
wait_for(
aws_object: instance,
query_method: :db_instance_status,
expected_responses: ['available'],
tries: new_resource.wait_tries,
sleep: new_resource.wait_time
) { |instance|
instance.reload
Chef::Log.info "Update RDS instance, waiting for #{new_resource.db_instance_identifier} to be available. State: #{instance.db_instance_status} - pending: #{instance.pending_modified_values.to_h}"
}
end
end
end
end

end #def update

def create_aws_object

## remove modify specific options we can't pass to create
[:apply_immediately, :allow_major_version_upgrade, :ca_certificate_identifier ].each do |key|
options_hash.delete(key)
end

Chef::Log.info "Create RDS instance: #{new_resource.db_instance_identifier}"
instance={}
converge_by "create RDS instance #{new_resource.db_instance_identifier} in #{region}" do
new_resource.driver.rds_resource.create_db_instance(options_hash)
instance=new_resource.driver.rds_resource.create_db_instance(options_hash)
end
end

if new_resource.wait_for_create
converge_by "waiting until RDS instance is available after create #{new_resource.db_instance_identifier} in #{region}" do
## custom wait loop - we can't use wait_for because we want to check for multiple possibilities, and some of them are undef at the time we start the loop.
## wait for:
## endpoint address to be available - at this point, the instance is typically usable. we get access to the instance a good 1000+s earlier than we would waiting for available.
## available or backing-up states, just in case we can't/dont get an endpoint address for some reason.
#just in case - sometimes instance is still nil when we get here, so avoid error cases
tries = 10
while instance.nil?
sleep 10
tries -= 1
raise "timed out waiting for #{new_resource.db_instance_identifier} instance object to become non-nil, something failed" if tries < 0
end
tries = new_resource.wait_tries
while defined?(instance.endpoint).nil? \
or defined?(instance.endpoint.address).nil? \
or instance.db_instance_status == 'available' \
or instance.db_instance_status == 'backing-up'
instance.reload #reload first so we get a useful final log
Chef::Log.info "Create RDS instance: waiting for #{new_resource.db_instance_identifier} to be available. State: #{instance.db_instance_status}, pending modifications: #{instance.pending_modified_values.to_h}, endpoint: #{instance.endpoint.to_h if ! instance.endpoint.nil? }"
sleep new_resource.wait_time
tries -= 1
raise StatusTimeoutError.new(instance, instance.db_instance_status, "endpoint available, 'available', or 'backing-up'") if tries < 0
end
Chef::Log.info "Create RDS instance: #{new_resource.db_instance_identifier} endpoint address = #{instance.endpoint.address}:#{instance.endpoint.port}"
end
end # end wait?
end #def create

def destroy_aws_object(instance)

### No need to wait before destroy - destroy doesnt require an available/etc state.
converge_by "delete RDS instance #{new_resource.db_instance_identifier} in #{region}" do
instance.delete(skip_final_snapshot: true)
instance.delete(skip_final_snapshot: new_resource.skip_final_snapshot)
end
# Wait up to 10 minutes for the db instance to shutdown
converge_by "waited until RDS instance #{new_resource.name} was deleted" do
wait_for(
aws_object: instance,
# http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Status.html
# It cannot _actually_ return a deleted status, we're just looking for the error
query_method: :db_instance_status,
expected_responses: ['deleted'],
acceptable_errors: [::Aws::RDS::Errors::DBInstanceNotFound],
tries: 60,
sleep: 10
) { |instance| instance.reload }
if new_resource.wait_for_delete
# Wait up to sleep * tries / 60 minutes for the db instance to shutdown
converge_by "waited until RDS instance #{new_resource.name} was deleted" do
wait_for(
aws_object: instance,
# http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Status.html
# It cannot _actually_ return a deletsed status, we're just looking for the error
query_method: :db_instance_status,
expected_responses: ['deleted'],
acceptable_errors: [::Aws::RDS::Errors::DBInstanceNotFound],
tries: new_resource.wait_tries,
sleep: new_resource.wait_time
) { |instance|
instance.reload
Chef::Log.info "Delete RDS instance: waiting for #{new_resource.db_instance_identifier} to be deleted. State: #{instance.db_instance_status}"
}
end
end
end
end #def destroy

# Sets the additional options then overrides it with all required options from
# the resource as well as optional options
Expand All @@ -66,3 +165,4 @@ def options_hash
end

end

14 changes: 14 additions & 0 deletions lib/chef/resource/aws_rds_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Chef::Resource::AwsRdsInstance < Chef::Provisioning::AWSDriver::AWSRDSReso

aws_sdk_type ::Aws::RDS::DBInstance, id: :db_instance_identifier

## first class attributes for RDS parameters
attribute :db_instance_identifier, kind_of: String, name_attribute: true

attribute :engine, kind_of: String
Expand All @@ -30,6 +31,19 @@ class Chef::Resource::AwsRdsInstance < Chef::Provisioning::AWSDriver::AWSRDSReso
# custom Hash
attribute :additional_options, kind_of: Hash, default: {}

## aws_rds_instance specific attributes
##the existing state
attribute :wait_for_create, kind_of: [TrueClass, FalseClass], default: false
attribute :wait_for_delete, kind_of: [TrueClass, FalseClass], default: true
#and new - wait for update by default
attribute :wait_for_update, kind_of: [TrueClass, FalseClass], default: true
# when we wait - how times we retry and how long we sleep between retries
# this is long by default because a lot of modifications, ie instance up/downgrade, take a long time.
attribute :wait_time, kind_of: Integer, default: 10
attribute :wait_tries, kind_of: Integer, default: 600

attribute :skip_final_snapshot, kind_of: [TrueClass, FalseClass], default: true

def aws_object
result = self.driver.rds_resource.db_instance(name)
return nil unless result && result.db_instance_status != 'deleting'
Expand Down