chef-provisioning-aws 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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