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.
- checksums.yaml +4 -4
- data/Gemfile +8 -0
- data/README.md +26 -39
- data/Rakefile +13 -5
- data/lib/chef/provider/aws_iam_instance_profile.rb +60 -0
- data/lib/chef/provider/aws_iam_role.rb +98 -0
- data/lib/chef/provider/aws_image.rb +1 -1
- data/lib/chef/provider/aws_internet_gateway.rb +75 -0
- data/lib/chef/provider/aws_route_table.rb +3 -2
- data/lib/chef/provider/aws_s3_bucket.rb +4 -1
- data/lib/chef/provider/aws_security_group.rb +1 -1
- data/lib/chef/provider/aws_vpc.rb +50 -45
- data/lib/chef/provisioning/aws_driver.rb +22 -1
- data/lib/chef/provisioning/aws_driver/aws_provider.rb +13 -5
- data/lib/chef/provisioning/aws_driver/aws_resource.rb +173 -165
- data/lib/chef/provisioning/aws_driver/credentials.rb +12 -0
- data/lib/chef/provisioning/aws_driver/driver.rb +82 -37
- data/lib/chef/provisioning/aws_driver/super_lwrp.rb +56 -43
- data/lib/chef/provisioning/aws_driver/version.rb +1 -1
- data/lib/chef/resource/aws_dhcp_options.rb +1 -1
- data/lib/chef/resource/aws_ebs_volume.rb +1 -1
- data/lib/chef/resource/aws_eip_address.rb +1 -1
- data/lib/chef/resource/aws_iam_instance_profile.rb +33 -0
- data/lib/chef/resource/aws_iam_role.rb +55 -0
- data/lib/chef/resource/aws_image.rb +1 -1
- data/lib/chef/resource/aws_instance.rb +1 -1
- data/lib/chef/resource/aws_internet_gateway.rb +36 -6
- data/lib/chef/resource/aws_load_balancer.rb +1 -1
- data/lib/chef/resource/aws_network_acl.rb +1 -1
- data/lib/chef/resource/aws_network_interface.rb +1 -1
- data/lib/chef/resource/aws_route53_hosted_zone.rb +261 -0
- data/lib/chef/resource/aws_route53_record_set.rb +162 -0
- data/lib/chef/resource/aws_route_table.rb +1 -1
- data/lib/chef/resource/aws_security_group.rb +1 -1
- data/lib/chef/resource/aws_sns_topic.rb +1 -1
- data/lib/chef/resource/aws_subnet.rb +1 -1
- data/lib/chef/resource/aws_vpc.rb +1 -1
- data/lib/chef/resource/aws_vpc_peering_connection.rb +1 -1
- data/spec/aws_support.rb +11 -13
- data/spec/aws_support/matchers/create_an_aws_object.rb +7 -1
- data/spec/aws_support/matchers/have_aws_object_tags.rb +1 -1
- data/spec/aws_support/matchers/match_an_aws_object.rb +7 -1
- data/spec/aws_support/matchers/update_an_aws_object.rb +8 -2
- data/spec/integration/aws_eip_address_spec.rb +74 -0
- data/spec/integration/aws_iam_instance_profile_spec.rb +159 -0
- data/spec/integration/aws_iam_role_spec.rb +177 -0
- data/spec/integration/aws_internet_gateway_spec.rb +161 -0
- data/spec/integration/aws_network_interface_spec.rb +3 -4
- data/spec/integration/aws_route53_hosted_zone_spec.rb +522 -0
- data/spec/integration/aws_route_table_spec.rb +52 -4
- data/spec/integration/aws_s3_bucket_spec.rb +1 -1
- data/spec/integration/load_balancer_spec.rb +303 -8
- data/spec/integration/machine_batch_spec.rb +1 -0
- data/spec/integration/machine_image_spec.rb +32 -17
- data/spec/integration/machine_spec.rb +11 -29
- data/spec/unit/chef/provisioning/aws_driver/driver_spec.rb +0 -1
- data/spec/unit/chef/provisioning/aws_driver/route53_spec.rb +105 -0
- 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,
|
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,
|
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
|
-
|
2
|
-
|
3
|
-
|
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,
|
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
|
-
|
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
|
-
|
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,
|
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
|
|
@@ -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,
|
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
|