Class: Chef::Provider::AwsRoute53HostedZone

Inherits:
Chef::Provisioning::AWSDriver::AWSProvider show all
Defined in:
lib/chef/resource/aws_route53_hosted_zone.rb

Constant Summary collapse

CREATE =
"CREATE".freeze
UPDATE =
UPSERT = "UPSERT".freeze
DELETE =
"DELETE".freeze
RRS_COMMENT =
"Managed by chef-provisioning-aws".freeze

Constants inherited from Chef::Provisioning::AWSDriver::AWSProvider

Chef::Provisioning::AWSDriver::AWSProvider::AWSResource

Instance Attribute Summary collapse

Attributes inherited from Chef::Provisioning::AWSDriver::AWSProvider

#purging

Instance Method Summary collapse

Methods inherited from Chef::Provisioning::AWSDriver::AWSProvider

#action_handler, #converge_by, #region, #whyrun_supported?

Instance Attribute Details

#record_set_listObject

Returns the value of attribute record_set_list.



88
89
90
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 88

def record_set_list
  @record_set_list
end

Instance Method Details

#create_aws_objectObject



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 107

def create_aws_object
  converge_by "create new Route 53 zone #{new_resource}" do
    # AWS stores some attributes off to the side here.
    hosted_zone_config = make_hosted_zone_config(new_resource)

    values = {
      name: new_resource.name,
      hosted_zone_config: hosted_zone_config,
      caller_reference: "chef-provisioning-aws-#{SecureRandom.uuid.upcase}", # required, unique each call
    }

    # this will validate the record_set resources prior to making any AWS calls.
    record_set_resources = get_record_sets_from_resource(new_resource)

    zone = new_resource.driver.route53_client.create_hosted_zone(values).hosted_zone
    new_resource.aws_route53_zone_id(zone.id)

    if record_set_resources
      populate_zone_info(record_set_resources, zone)

      change_list = record_set_resources.map { |rs| rs.to_aws_change_struct(UPDATE) }

      new_resource.driver.route53_client.change_resource_record_sets(hosted_zone_id: new_resource.aws_route53_zone_id,
                                                                     change_batch: {
                                                                       comment: RRS_COMMENT,
                                                                       changes: change_list
                                                                     })
    end
    zone
  end
end

#destroy_aws_object(hosted_zone) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 198

def destroy_aws_object(hosted_zone)
  converge_by "delete Route53 zone #{new_resource}" do
    Chef::Log.info("Deleting all non-SOA/NS records for #{hosted_zone.name}")

    rr_changes = hosted_zone.resource_record_sets.reject do |aws_rr|
                   %w{SOA NS}.include?(aws_rr.type)
                 end.map do |aws_rr|
      {
        action: DELETE,
        resource_record_set: aws_rr.to_change_struct
      }
    end

    unless rr_changes.empty?
      aws_struct = {
        hosted_zone_id: hosted_zone.id,
        change_batch: {
          comment: "Purging RRs prior to deleting resource",
          changes: rr_changes
        }
      }

      new_resource.driver.route53_client.change_resource_record_sets(aws_struct)
    end

    result = new_resource.driver.route53_client.delete_hosted_zone(id: hosted_zone.id)
  end
end

#get_record_sets_from_resource(new_resource) ⇒ Object

‘record_sets` is defined on the `aws_route53_hosted_zone` resource as a block attribute, so compile that, validate it, and return a list of AWSRoute53RecordSet resource objects.



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 229

def get_record_sets_from_resource(new_resource)
  return nil unless new_resource.record_sets
  instance_eval(&new_resource.record_sets)

  # because we're in the provider, the RecordSet resources happen in their own mini Chef run, and they're the
  # only things in the resource_collection.
  record_set_resources = run_context.resource_collection.to_a
  return nil unless record_set_resources

  record_set_resources.each do |rs|
    rs.aws_route53_hosted_zone(new_resource)
    rs.aws_route53_zone_name(new_resource.name)

    if new_resource.defaults
      new_resource.class::DEFAULTABLE_ATTRS.each do |att|
        # check if the RecordSet has its own value, without triggering validation. in Chef >= 12.5, there is
        # #property_is_set?.
        if rs.instance_variable_get("@#{att}").nil? && !new_resource.defaults[att].nil?
          rs.send(att, new_resource.defaults[att])
        end
      end
    end

    rs.validate!
  end

  Chef::Resource::AwsRoute53RecordSet.verify_unique!(record_set_resources)
  record_set_resources
end

#make_hosted_zone_config(new_resource) ⇒ Object



90
91
92
93
94
95
96
97
98
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 90

def make_hosted_zone_config(new_resource)
  config = {}
  # add :private_zone here once VPC validation is enabled.
  [:comment].each do |attr|
    value = new_resource.send(attr)
    config[attr] = value if value
  end
  config
end

#populate_zone_info(record_set_resources, hosted_zone) ⇒ Object

this happens at a slightly different time in the lifecycle from #get_record_sets_from_resource.



101
102
103
104
105
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 101

def populate_zone_info(record_set_resources, hosted_zone)
  record_set_resources.each do |rs|
    rs.aws_route53_zone_id(hosted_zone.id)
  end
end

#update_aws_object(hosted_zone) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/chef/resource/aws_route53_hosted_zone.rb', line 139

def update_aws_object(hosted_zone)
  new_resource.aws_route53_zone_id(hosted_zone.id)

  # this will validate the record_set resources prior to making any AWS calls.
  record_set_resources = get_record_sets_from_resource(new_resource)

  if new_resource.comment != hosted_zone.config.comment
    new_resource.driver.route53_client.update_hosted_zone_comment(id: hosted_zone.id, comment: new_resource.comment)
  end

  if record_set_resources
    populate_zone_info(record_set_resources, hosted_zone)

    aws_record_sets = hosted_zone.resource_record_sets

    change_list = []

    # TODO: the SOA and NS records have identical :name properties (the zone name), so one of them will
    # be overwritten in the `keyed_aws_objects` hash. mostly we're declining to operate on SOA and NS,
    # so it probably doesn't matter, but bears investigating.

    # we already checked for duplicate Chef RR resources in #get_record_sets_from_resource.
    keyed_chef_resources = record_set_resources.each_with_object({}) { |rs, coll| (coll[rs.aws_key] ||= []) << rs; }
    keyed_aws_objects    = aws_record_sets.each_with_object({})      { |rs, coll| coll[rs.aws_key] = rs; }

    # because DNS is important, we're going to err on the side of caution and only operate on records for
    # which we have a Chef resource. "total management" might be a nice resource option to have.
    keyed_chef_resources.each do |key, chef_resource_ary|
      chef_resource_ary.each do |chef_resource|
        # RR already exists...
        if keyed_aws_objects.key?(key)
          # ... do we want to delete it?
          if chef_resource.action.first == :destroy
            change_list << chef_resource.to_aws_change_struct(DELETE)
          # ... update it, then, only if the fields differ.
          elsif chef_resource.to_aws_struct != keyed_aws_objects[key]
            change_list << chef_resource.to_aws_change_struct(UPDATE)
          end
        # otherwise, RR does not already exist...
        else
          # using UPSERT instead of CREATE; there are merits to both.
          change_list << chef_resource.to_aws_change_struct(UPSERT)
        end
      end
    end

    Chef::Log.debug("RecordSet changes: #{change_list.inspect}")
    if !change_list.empty?
      new_resource.driver.route53_client.change_resource_record_sets(hosted_zone_id: new_resource.aws_route53_zone_id,
                                                                     change_batch: {
                                                                       comment: RRS_COMMENT,
                                                                       changes: change_list
                                                                     })
    else
      Chef::Log.info("All aws_route53_record_set resources up to date (nothing to do).")
    end
  end
end