chef-provisioning-aws 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -0
  3. data/README.md +26 -39
  4. data/Rakefile +13 -5
  5. data/lib/chef/provider/aws_iam_instance_profile.rb +60 -0
  6. data/lib/chef/provider/aws_iam_role.rb +98 -0
  7. data/lib/chef/provider/aws_image.rb +1 -1
  8. data/lib/chef/provider/aws_internet_gateway.rb +75 -0
  9. data/lib/chef/provider/aws_route_table.rb +3 -2
  10. data/lib/chef/provider/aws_s3_bucket.rb +4 -1
  11. data/lib/chef/provider/aws_security_group.rb +1 -1
  12. data/lib/chef/provider/aws_vpc.rb +50 -45
  13. data/lib/chef/provisioning/aws_driver.rb +22 -1
  14. data/lib/chef/provisioning/aws_driver/aws_provider.rb +13 -5
  15. data/lib/chef/provisioning/aws_driver/aws_resource.rb +173 -165
  16. data/lib/chef/provisioning/aws_driver/credentials.rb +12 -0
  17. data/lib/chef/provisioning/aws_driver/driver.rb +82 -37
  18. data/lib/chef/provisioning/aws_driver/super_lwrp.rb +56 -43
  19. data/lib/chef/provisioning/aws_driver/version.rb +1 -1
  20. data/lib/chef/resource/aws_dhcp_options.rb +1 -1
  21. data/lib/chef/resource/aws_ebs_volume.rb +1 -1
  22. data/lib/chef/resource/aws_eip_address.rb +1 -1
  23. data/lib/chef/resource/aws_iam_instance_profile.rb +33 -0
  24. data/lib/chef/resource/aws_iam_role.rb +55 -0
  25. data/lib/chef/resource/aws_image.rb +1 -1
  26. data/lib/chef/resource/aws_instance.rb +1 -1
  27. data/lib/chef/resource/aws_internet_gateway.rb +36 -6
  28. data/lib/chef/resource/aws_load_balancer.rb +1 -1
  29. data/lib/chef/resource/aws_network_acl.rb +1 -1
  30. data/lib/chef/resource/aws_network_interface.rb +1 -1
  31. data/lib/chef/resource/aws_route53_hosted_zone.rb +261 -0
  32. data/lib/chef/resource/aws_route53_record_set.rb +162 -0
  33. data/lib/chef/resource/aws_route_table.rb +1 -1
  34. data/lib/chef/resource/aws_security_group.rb +1 -1
  35. data/lib/chef/resource/aws_sns_topic.rb +1 -1
  36. data/lib/chef/resource/aws_subnet.rb +1 -1
  37. data/lib/chef/resource/aws_vpc.rb +1 -1
  38. data/lib/chef/resource/aws_vpc_peering_connection.rb +1 -1
  39. data/spec/aws_support.rb +11 -13
  40. data/spec/aws_support/matchers/create_an_aws_object.rb +7 -1
  41. data/spec/aws_support/matchers/have_aws_object_tags.rb +1 -1
  42. data/spec/aws_support/matchers/match_an_aws_object.rb +7 -1
  43. data/spec/aws_support/matchers/update_an_aws_object.rb +8 -2
  44. data/spec/integration/aws_eip_address_spec.rb +74 -0
  45. data/spec/integration/aws_iam_instance_profile_spec.rb +159 -0
  46. data/spec/integration/aws_iam_role_spec.rb +177 -0
  47. data/spec/integration/aws_internet_gateway_spec.rb +161 -0
  48. data/spec/integration/aws_network_interface_spec.rb +3 -4
  49. data/spec/integration/aws_route53_hosted_zone_spec.rb +522 -0
  50. data/spec/integration/aws_route_table_spec.rb +52 -4
  51. data/spec/integration/aws_s3_bucket_spec.rb +1 -1
  52. data/spec/integration/load_balancer_spec.rb +303 -8
  53. data/spec/integration/machine_batch_spec.rb +1 -0
  54. data/spec/integration/machine_image_spec.rb +32 -17
  55. data/spec/integration/machine_spec.rb +11 -29
  56. data/spec/unit/chef/provisioning/aws_driver/driver_spec.rb +0 -1
  57. data/spec/unit/chef/provisioning/aws_driver/route53_spec.rb +105 -0
  58. metadata +48 -6
@@ -0,0 +1,55 @@
1
+ require 'chef/provisioning/aws_driver/aws_resource'
2
+
3
+ #
4
+ # An AWS IAM role, specifying set of policies for acessing other AWS services.
5
+ #
6
+ # `name` is unique for an AWS account.
7
+ #
8
+ # API documentation for the AWS Ruby SDK for IAM roles (and the object returned from `aws_object`) can be found here:
9
+ #
10
+ # - http://docs.aws.amazon.com/sdkforruby/api/Aws/IAM.html
11
+ #
12
+ class Chef::Resource::AwsIamRole < Chef::Provisioning::AWSDriver::AWSResource
13
+ aws_sdk_type ::Aws::IAM::Role
14
+
15
+ #
16
+ # The name of the role to create.
17
+ #
18
+ attribute :name, kind_of: String, name_attribute: true
19
+
20
+ #
21
+ # The path to the role. For more information about paths, see http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html
22
+ #
23
+ attribute :path, kind_of: String
24
+
25
+ #
26
+ # The policy that grants an entity permission to assume the role.
27
+ #
28
+ attribute :assume_role_policy_document, kind_of: String
29
+
30
+ #
31
+ # Inline policies which _only_ apply to this role, unlike managed_policies
32
+ # which can be shared between users, groups and roles. Maps to the
33
+ # [RolePolicy](http://docs.aws.amazon.com/sdkforruby/api/Aws/IAM/RolePolicy.html)
34
+ # SDK object.
35
+ #
36
+ # Hash keys are the inline policy name and the value is the policy document.
37
+ #
38
+ attribute :inline_policies, kind_of: Hash, callbacks: {
39
+ "inline_policies must be a hash maping policy names to policy documents" => proc do |policies|
40
+ policies.all? {|policy_name, policy| (policy_name.is_a?(String) || policy_name.is_a?(Symbol)) && policy.is_a?(String)}
41
+ end
42
+ }
43
+
44
+ #
45
+ # TODO: add when we get a policy resource
46
+ #
47
+ # attribute :managed_policies, kind_of: [Array, String, ::Aws::Iam::Policy, AwsIamPolicy], coerce: proc { |value| [value].flatten }
48
+
49
+ def aws_object
50
+ driver.iam_resource.role(name).load
51
+ rescue ::Aws::IAM::Errors::NoSuchEntity
52
+ nil
53
+ end
54
+
55
+ end
@@ -10,7 +10,7 @@ class Chef::Resource::AwsImage < Chef::Provisioning::AWSDriver::AWSResourceWithE
10
10
 
11
11
  attribute :name, kind_of: String, name_attribute: true
12
12
 
13
- attribute :image_id, kind_of: String, aws_id_attribute: true, lazy_default: proc {
13
+ attribute :image_id, kind_of: String, aws_id_attribute: true, default: lazy {
14
14
  name =~ /^ami-[a-f0-9]{8}$/ ? name : nil
15
15
  }
16
16
 
@@ -12,7 +12,7 @@ class Chef::Resource::AwsInstance < Chef::Provisioning::AWSDriver::AWSResourceWi
12
12
 
13
13
  attribute :name, kind_of: String, name_attribute: true
14
14
 
15
- attribute :instance_id, kind_of: String, aws_id_attribute: true, lazy_default: proc {
15
+ attribute :instance_id, kind_of: String, aws_id_attribute: true, default: lazy {
16
16
  name =~ /^i-[a-f0-9]{8}$/ ? name : nil
17
17
  }
18
18
 
@@ -1,18 +1,48 @@
1
- require 'chef/provisioning/aws_driver/aws_resource'
2
-
3
- class Chef::Resource::AwsInternetGateway < Chef::Provisioning::AWSDriver::AWSResource
1
+ #
2
+ # An AWS internet gateway, allowing communication between instances inside a VPC and the internet.
3
+ #
4
+ # `name` is not guaranteed unique for an AWS account; therefore, Chef will
5
+ # store the internet gateway ID associated with this name in your Chef server in the
6
+ # data bag `data/aws_internet_gateway/<name>`.
7
+ #
8
+ # API documentation for the AWS Ruby SDK for VPCs (and the object returned from `aws_object` can be found here:
9
+ #
10
+ # - http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/InternetGateway.html
11
+ #
12
+ class Chef::Resource::AwsInternetGateway < Chef::Provisioning::AWSDriver::AWSResourceWithEntry
4
13
  include Chef::Provisioning::AWSDriver::AWSTaggable
5
14
 
6
- aws_sdk_type AWS::EC2::InternetGateway, load_provider: false, id: :id
15
+ aws_sdk_type AWS::EC2::InternetGateway, id: :id
16
+
17
+ require 'chef/resource/aws_vpc'
7
18
 
19
+ #
20
+ # Extend actions for the internet gateway
21
+ #
22
+ actions :create, :destroy, :detach, :purge
23
+
24
+ #
25
+ # The name of this internet gateway.
26
+ #
8
27
  attribute :name, kind_of: String, name_attribute: true
9
28
 
10
- attribute :internet_gateway_id, kind_of: String, aws_id_attribute: true, lazy_default: proc {
29
+ #
30
+ # A vpc to attach to the internet gateway.
31
+ #
32
+ # May be one of:
33
+ # - The name of an `aws_vpc` Chef resource.
34
+ # - An actual `aws_vpc` resource.
35
+ # - An AWS `VPC` object.
36
+ #
37
+ attribute :vpc, kind_of: [ String, AwsVpc, AWS::EC2::VPC ]
38
+
39
+ attribute :internet_gateway_id, kind_of: String, aws_id_attribute: true, default: lazy {
11
40
  name =~ /^igw-[a-f0-9]{8}$/ ? name : nil
12
41
  }
13
42
 
14
43
  def aws_object
15
- result = driver.ec2.internet_gateways[internet_gateway_id]
44
+ driver, id = get_driver_and_id
45
+ result = driver.ec2.internet_gateways[id] if id
16
46
  result && result.exists? ? result : nil
17
47
  end
18
48
  end
@@ -8,7 +8,7 @@ class Chef::Resource::AwsLoadBalancer < Chef::Provisioning::AWSDriver::AWSResour
8
8
 
9
9
  attribute :name, kind_of: String, name_attribute: true
10
10
 
11
- attribute :load_balancer_id, kind_of: String, aws_id_attribute: true, lazy_default: proc {
11
+ attribute :load_balancer_id, kind_of: String, aws_id_attribute: true, default: lazy {
12
12
  name =~ /^elb-[a-f0-9]{8}$/ ? name : nil
13
13
  }
14
14
 
@@ -44,7 +44,7 @@ class Chef::Resource::AwsNetworkAcl < Chef::Provisioning::AWSDriver::AWSResource
44
44
  attribute :network_acl_id,
45
45
  kind_of: String,
46
46
  aws_id_attribute: true,
47
- lazy_default: proc {
47
+ default: lazy {
48
48
  name =~ /^acl-[a-f0-9]{8}$/ ? name : nil
49
49
  }
50
50
 
@@ -9,7 +9,7 @@ class Chef::Resource::AwsNetworkInterface < Chef::Provisioning::AWSDriver::AWSRe
9
9
 
10
10
  attribute :name, kind_of: String, name_attribute: true
11
11
 
12
- attribute :network_interface_id, kind_of: String, aws_id_attribute: true, lazy_default: proc {
12
+ attribute :network_interface_id, kind_of: String, aws_id_attribute: true, default: lazy {
13
13
  name =~ /^eni-[a-f0-9]{8}$/ ? name : nil
14
14
  }
15
15
 
@@ -0,0 +1,261 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2015 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'chef/provisioning/aws_driver/aws_resource'
19
+ require 'chef/resource/aws_route53_record_set'
20
+ require 'securerandom'
21
+
22
+ # the AWS API doesn't have these objects linked, so give it some help.
23
+ class Aws::Route53::Types::HostedZone
24
+ attr_accessor :resource_record_sets
25
+ end
26
+
27
+ class Chef::Resource::AwsRoute53HostedZone < Chef::Provisioning::AWSDriver::AWSResourceWithEntry
28
+
29
+ aws_sdk_type ::Aws::Route53::Types::HostedZone, load_provider: false
30
+
31
+ resource_name :aws_route53_hosted_zone
32
+
33
+ # name of the domain. AWS will tack on a trailing dot, so we're going to prohibit it here for consistency:
34
+ # the name is our data bag key, and if a user has "foo.com" in one resource and "foo.com." in another, Route
35
+ # 53 will happily accept two different domains it calls "foo.com.".
36
+ attribute :name, kind_of: String, callbacks: { "domain name cannot end with a dot" => lambda { |n| n !~ /\.$/ } }
37
+
38
+ # The comment included in the CreateHostedZoneRequest element. String <= 256 characters.
39
+ attribute :comment, kind_of: String
40
+
41
+ # the resource name and the AWS ID have to be related here, since they're tightly coupled elsewhere.
42
+ attribute :aws_route53_zone_id, kind_of: String, aws_id_attribute: true,
43
+ default: lazy { name =~ /^\/hostedzone\// ? name : nil }
44
+
45
+ DEFAULTABLE_ATTRS = [:ttl, :type]
46
+
47
+ attribute :defaults, kind_of: Hash,
48
+ callbacks: { "'defaults' keys may be any of #{DEFAULTABLE_ATTRS}" => lambda { |dh|
49
+ (dh.keys - DEFAULTABLE_ATTRS).size == 0 } }
50
+
51
+ def record_sets(&block)
52
+ if block_given?
53
+ @record_sets_block = block
54
+ else
55
+ @record_sets_block
56
+ end
57
+ end
58
+
59
+ def aws_object
60
+ driver, id = get_driver_and_id
61
+ result = driver.route53_client.get_hosted_zone(id: id).hosted_zone if id rescue nil
62
+ if result
63
+ result.resource_record_sets = get_record_sets_from_aws(result.id).resource_record_sets
64
+ result
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ # since this is used exactly once, it could plausibly be inlined in #aws_object.
71
+ def get_record_sets_from_aws(hosted_zone_id, opts={})
72
+ params = { hosted_zone_id: hosted_zone_id }.merge(opts)
73
+ driver.route53_client.list_resource_record_sets(params)
74
+ end
75
+ end
76
+
77
+ class Chef::Provider::AwsRoute53HostedZone < Chef::Provisioning::AWSDriver::AWSProvider
78
+
79
+ provides :aws_route53_hosted_zone
80
+ use_inline_resources
81
+
82
+ CREATE = "CREATE"
83
+ UPDATE = UPSERT = "UPSERT"
84
+ DELETE = "DELETE"
85
+ RRS_COMMENT = "Managed by chef-provisioning-aws"
86
+
87
+ attr_accessor :record_set_list
88
+
89
+ def make_hosted_zone_config(new_resource)
90
+ config = {}
91
+ # add :private_zone here once VPC validation is enabled.
92
+ [:comment].each do |attr|
93
+ value = new_resource.send(attr)
94
+ if value
95
+ config[attr] = value
96
+ end
97
+ end
98
+ config
99
+ end
100
+
101
+ # this happens at a slightly different time in the lifecycle from #get_record_sets_from_resource.
102
+ def populate_zone_info(record_set_resources, hosted_zone)
103
+ record_set_resources.each do |rs|
104
+ rs.aws_route53_zone_id(hosted_zone.id)
105
+ end
106
+ end
107
+
108
+ def create_aws_object
109
+ converge_by "create new Route 53 zone #{new_resource}" do
110
+
111
+ # AWS stores some attributes off to the side here.
112
+ hosted_zone_config = make_hosted_zone_config(new_resource)
113
+
114
+ values = {
115
+ name: new_resource.name,
116
+ hosted_zone_config: hosted_zone_config,
117
+ caller_reference: "chef-provisioning-aws-#{SecureRandom.uuid.upcase}", # required, unique each call
118
+ }
119
+
120
+ # this will validate the record_set resources prior to making any AWS calls.
121
+ record_set_resources = get_record_sets_from_resource(new_resource)
122
+
123
+ zone = new_resource.driver.route53_client.create_hosted_zone(values).hosted_zone
124
+ new_resource.aws_route53_zone_id(zone.id)
125
+
126
+ if record_set_resources
127
+ populate_zone_info(record_set_resources, zone)
128
+
129
+ change_list = record_set_resources.map { |rs| rs.to_aws_change_struct(CREATE) }
130
+
131
+ new_resource.driver.route53_client.change_resource_record_sets(hosted_zone_id: new_resource.aws_route53_zone_id,
132
+ change_batch: {
133
+ comment: RRS_COMMENT,
134
+ changes: change_list,
135
+ })
136
+ end
137
+ zone
138
+ end
139
+ end
140
+
141
+ def update_aws_object(hosted_zone)
142
+ new_resource.aws_route53_zone_id(hosted_zone.id)
143
+
144
+ # this will validate the record_set resources prior to making any AWS calls.
145
+ record_set_resources = get_record_sets_from_resource(new_resource)
146
+
147
+ if new_resource.comment != hosted_zone.config.comment
148
+ new_resource.driver.route53_client.update_hosted_zone_comment(id: hosted_zone.id, comment: new_resource.comment)
149
+ end
150
+
151
+ if record_set_resources
152
+ populate_zone_info(record_set_resources, hosted_zone)
153
+
154
+ aws_record_sets = hosted_zone.resource_record_sets
155
+
156
+ change_list = []
157
+
158
+ # TODO: the SOA and NS records have identical :name properties (the zone name), so one of them will
159
+ # be overwritten in the `keyed_aws_objects` hash. mostly we're declining to operate on SOA and NS,
160
+ # so it probably doesn't matter, but bears investigating.
161
+
162
+ # we already checked for duplicate Chef RR resources in #get_record_sets_from_resource.
163
+ keyed_chef_resources = record_set_resources.reduce({}) { |coll, rs| (coll[rs.aws_key] ||= []) << rs; coll }
164
+ keyed_aws_objects = aws_record_sets.reduce({}) { |coll, rs| coll[rs.aws_key] = rs; coll }
165
+
166
+ # because DNS is important, we're going to err on the side of caution and only operate on records for
167
+ # which we have a Chef resource. "total management" might be a nice resource option to have.
168
+ keyed_chef_resources.each do |key, chef_resource_ary|
169
+ chef_resource_ary.each do |chef_resource|
170
+ # RR already exists...
171
+ if keyed_aws_objects.has_key?(key)
172
+ # ... do we want to delete it?
173
+ if chef_resource.action.first == :destroy
174
+ change_list << chef_resource.to_aws_change_struct(DELETE)
175
+ # ... update it, then, only if the fields differ.
176
+ elsif chef_resource.to_aws_struct != keyed_aws_objects[key]
177
+ change_list << chef_resource.to_aws_change_struct(UPDATE)
178
+ end
179
+ # otherwise, RR does not already exist...
180
+ else
181
+ # using UPSERT instead of CREATE; there are merits to both.
182
+ change_list << chef_resource.to_aws_change_struct(UPSERT)
183
+ end
184
+ end
185
+ end
186
+
187
+ Chef::Log.debug("RecordSet changes: #{change_list.inspect}")
188
+ if change_list.size > 0
189
+ new_resource.driver.route53_client.change_resource_record_sets(hosted_zone_id: new_resource.aws_route53_zone_id,
190
+ change_batch: {
191
+ comment: RRS_COMMENT,
192
+ changes: change_list,
193
+ })
194
+ else
195
+ Chef::Log.info("All aws_route53_record_set resources up to date (nothing to do).")
196
+ end
197
+ end
198
+ end
199
+
200
+ def destroy_aws_object(hosted_zone)
201
+ converge_by "delete Route53 zone #{new_resource}" do
202
+ Chef::Log.info("Deleting all non-SOA/NS records for #{hosted_zone.name}")
203
+
204
+ rr_changes = hosted_zone.resource_record_sets.reject { |aws_rr|
205
+ %w{SOA NS}.include?(aws_rr.type)
206
+ }.map { |aws_rr|
207
+ {
208
+ action: DELETE,
209
+ resource_record_set: aws_rr.to_change_struct,
210
+ }
211
+ }
212
+
213
+ if rr_changes.size > 0
214
+ aws_struct = {
215
+ hosted_zone_id: hosted_zone.id,
216
+ change_batch: {
217
+ comment: "Purging RRs prior to deleting resource",
218
+ changes: rr_changes,
219
+ }
220
+ }
221
+
222
+ new_resource.driver.route53_client.change_resource_record_sets(aws_struct)
223
+ end
224
+
225
+ result = new_resource.driver.route53_client.delete_hosted_zone(id: hosted_zone.id)
226
+ end
227
+ end
228
+
229
+ # `record_sets` is defined on the `aws_route53_hosted_zone` resource as a block attribute, so compile that,
230
+ # validate it, and return a list of AWSRoute53RecordSet resource objects.
231
+ def get_record_sets_from_resource(new_resource)
232
+
233
+ return nil unless new_resource.record_sets
234
+ instance_eval(&new_resource.record_sets)
235
+
236
+ # because we're in the provider, the RecordSet resources happen in their own mini Chef run, and they're the
237
+ # only things in the resource_collection.
238
+ record_set_resources = run_context.resource_collection.to_a
239
+ return nil unless record_set_resources
240
+
241
+ record_set_resources.each do |rs|
242
+ rs.aws_route53_hosted_zone(new_resource)
243
+ rs.aws_route53_zone_name(new_resource.name)
244
+
245
+ if new_resource.defaults
246
+ new_resource.class::DEFAULTABLE_ATTRS.each do |att|
247
+ # check if the RecordSet has its own value, without triggering validation. in Chef >= 12.5, there is
248
+ # #property_is_set?.
249
+ if rs.instance_variable_get("@#{att}").nil? && !new_resource.defaults[att].nil?
250
+ rs.send(att, new_resource.defaults[att])
251
+ end
252
+ end
253
+ end
254
+
255
+ rs.validate!
256
+ end
257
+
258
+ Chef::Resource::AwsRoute53RecordSet.verify_unique!(record_set_resources)
259
+ record_set_resources
260
+ end
261
+ end
@@ -0,0 +1,162 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2015 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ class Aws::Route53::Types::ResourceRecordSet
19
+ # removing AWS's trailing dots may not be the best thing, but otherwise our job gets much harder.
20
+ def aws_key
21
+ "#{name.sub(/\.$/, '')}"
22
+ end
23
+
24
+ # the API doesn't seem to provide any facility to convert these types into the data structures used by the
25
+ # API; see http://redirx.me/?t3za for the RecordSet type specifically.
26
+ def to_change_struct
27
+ {
28
+ name: name,
29
+ type: type,
30
+ ttl: ttl,
31
+ resource_records: resource_records.map {|r| {:value => r.value}},
32
+ }
33
+ end
34
+ end
35
+
36
+ class Chef::Resource::AwsRoute53RecordSet < Chef::Provisioning::AWSDriver::SuperLWRP
37
+
38
+ actions :create, :destroy
39
+ default_action :create
40
+
41
+ resource_name :aws_route53_record_set
42
+ attribute :aws_route53_zone_id, kind_of: String, required: true
43
+
44
+ attribute :rr_name, required: true
45
+
46
+ attribute :type, equal_to: %w(SOA A TXT NS CNAME MX PTR SRV SPF AAAA), required: true
47
+
48
+ attribute :ttl, kind_of: Fixnum, required: true
49
+
50
+ attribute :resource_records, kind_of: Array, required: true
51
+
52
+ # this gets set internally and is not intended for DSL use in recipes.
53
+ attribute :aws_route53_zone_name, kind_of: String, required: true,
54
+ is: lambda { |zone_name| validate_zone_name!(rr_name, zone_name) }
55
+
56
+ attribute :aws_route53_hosted_zone, required: true
57
+
58
+ def initialize(name, *args)
59
+ self.rr_name(name) unless @rr_name
60
+ super(name, *args)
61
+ end
62
+
63
+ def validate_rr_type!(type, rr_list)
64
+ case type
65
+ # we'll check for integers, but leave the user responsible for valid DNS names.
66
+ when "A"
67
+ rr_list.all? { |v| v =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ } ||
68
+ raise(::Chef::Exceptions::ValidationFailed,
69
+ "A records are of the form '141.2.25.3'")
70
+ when "MX"
71
+ rr_list.all? { |v| v =~ /^\d+\s+[^ ]+/} ||
72
+ raise(::Chef::Exceptions::ValidationFailed,
73
+ "MX records must have a priority and mail server, of the form '15 mail.example.com.'")
74
+ when "SRV"
75
+ rr_list.all? { |v| v =~ /^\d+\s+\d+\s+\d+\s+[^ ]+$/ } ||
76
+ raise(::Chef::Exceptions::ValidationFailed,
77
+ "SRV records must have a priority, weight, port, and hostname, of the form '15 10 25 service.example.com.'")
78
+ when "CNAME"
79
+ rr_list.size == 1 ||
80
+ raise(::Chef::Exceptions::ValidationFailed,
81
+ "CNAME records may only have a single value (a hostname).")
82
+
83
+ when "TXT", "PTR", "AAAA", "SPF"
84
+ true
85
+ else
86
+ raise ArgumentError, "Argument '#{type}' must be one of #{%w(A MX SRV CNAME TXT PTR AAAA SPF)}"
87
+ end
88
+ end
89
+
90
+ def validate_zone_name!(rr_name, zone_name)
91
+ if rr_name.end_with?('.') && rr_name !~ /#{zone_name}\.$/
92
+ raise(::Chef::Exceptions::ValidationFailed, "RecordSet name #{rr_name} does not match parent HostedZone name #{zone_name}.")
93
+ end
94
+ true
95
+ end
96
+
97
+ # because these resources can't actually converge themselves, we have to trigger the validations.
98
+ def validate!
99
+ [:rr_name, :type, :ttl, :resource_records, :aws_route53_zone_name].each { |f| self.send(f) }
100
+
101
+ # this was in an :is validator, but didn't play well with inheriting default values.
102
+ validate_rr_type!(type, resource_records)
103
+ end
104
+
105
+ def aws_key
106
+ "#{fqdn}"
107
+ end
108
+
109
+ def fqdn
110
+ if rr_name !~ /#{aws_route53_zone_name}\.?$/
111
+ "#{rr_name}.#{aws_route53_zone_name}"
112
+ else
113
+ rr_name
114
+ end
115
+ end
116
+
117
+ def to_aws_struct
118
+ {
119
+ name: fqdn,
120
+ type: type,
121
+ ttl: ttl,
122
+ resource_records: resource_records.map { |rr| { value: rr } },
123
+ }
124
+ end
125
+
126
+ def to_aws_change_struct(aws_action)
127
+ # there are more elements which are optional, notably 'weight' and 'region': see the API doc at
128
+ # http://redirx.me/?t3zo
129
+ {
130
+ action: aws_action,
131
+ resource_record_set: self.to_aws_struct
132
+ }
133
+ end
134
+
135
+ def self.verify_unique!(record_sets)
136
+ seen = {}
137
+
138
+ record_sets.each do |rs|
139
+ key = rs.aws_key
140
+ if seen.has_key?(key)
141
+ raise Chef::Exceptions::ValidationFailed.new("Duplicate RecordSet found in resource: [#{key}]")
142
+ else
143
+ seen[key] = 1
144
+ end
145
+ end
146
+
147
+ # TODO: be helpful and print out all duplicates, not just the first.
148
+
149
+ true
150
+ end
151
+ end
152
+
153
+ class Chef::Provider::AwsRoute53RecordSet < Chef::Provider::LWRPBase
154
+ provides :aws_route53_record_set
155
+
156
+ # to make RR changes in transactional batches, it has to be done in the parent resource.
157
+ action :create do
158
+ end
159
+
160
+ action :destroy do
161
+ end
162
+ end