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.
- 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
|