Class: Kitchen::Driver::Aws::InstanceGenerator
- Inherits:
-
Object
- Object
- Kitchen::Driver::Aws::InstanceGenerator
- Defined in:
- lib/kitchen/driver/aws/instance_generator.rb
Overview
A class for encapsulating the instance payload logic
Instance Attribute Summary collapse
-
#config ⇒ Hash
readonly
The driver config the payload is built from.
-
#ec2 ⇒ Kitchen::Driver::Aws::Client
readonly
The driver's EC2 client wrapper.
-
#logger ⇒ Kitchen::Logger
readonly
The logger to report through.
Instance Method Summary collapse
-
#ec2_instance_data ⇒ Hash
Build the RunInstances payload from the driver config.
-
#initialize(config, ec2, logger) ⇒ InstanceGenerator
constructor
A new instance of InstanceGenerator.
-
#prepared_user_data ⇒ String?
The user data script, base64 encoded as EC2 requires.
Constructor Details
#initialize(config, ec2, logger) ⇒ InstanceGenerator
Returns a new instance of InstanceGenerator.
41 42 43 44 45 |
# File 'lib/kitchen/driver/aws/instance_generator.rb', line 41 def initialize(config, ec2, logger) @config = config @ec2 = ec2 @logger = logger end |
Instance Attribute Details
#config ⇒ Hash (readonly)
Returns the driver config the payload is built from.
30 31 32 |
# File 'lib/kitchen/driver/aws/instance_generator.rb', line 30 def config @config end |
#ec2 ⇒ Kitchen::Driver::Aws::Client (readonly)
Returns the driver's EC2 client wrapper.
33 34 35 |
# File 'lib/kitchen/driver/aws/instance_generator.rb', line 33 def ec2 @ec2 end |
#logger ⇒ Kitchen::Logger (readonly)
Returns the logger to report through.
36 37 38 |
# File 'lib/kitchen/driver/aws/instance_generator.rb', line 36 def logger @logger end |
Instance Method Details
#ec2_instance_data ⇒ Hash
Build the RunInstances payload from the driver config.
Some EC2 fields accept an explicit nil and others must be omitted entirely, so optional settings are added conditionally rather than always being present with a nil value.
Two lookups happen here as a side effect, because both need to resolve
before the payload can be built: a subnet is resolved from
subnet_filter (and written back into the config), and security
groups are resolved from security_group_filter within that subnet's
VPC. Both are skipped when the corresponding ID is already set.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/kitchen/driver/aws/instance_generator.rb', line 64 def ec2_instance_data # Support for looking up security group id and subnet id using tags. vpc_id = nil client = ::Aws::EC2::Client.new(region: config[:region]) if config[:subnet_id].nil? && config[:subnet_filter] filters = [config[:subnet_filter]].flatten r = { filters: [] } filters.each do |subnet_filter| r[:filters] << { name: "tag:#{subnet_filter[:tag]}", values: [subnet_filter[:value]], } end subnets = client.describe_subnets(r).subnets raise "Subnets with tags '#{filters}' not found during security group creation" if subnets.empty? # => Select the least-populated subnet if we have multiple matches subnet = subnets.max_by { |s| s[:available_ip_address_count] } vpc_id = subnet.vpc_id config[:subnet_id] = subnet.subnet_id end if config[:security_group_ids].nil? && config[:security_group_filter] # => Grab the VPC in the case a Subnet ID rather than Filter was set vpc_id ||= client.describe_subnets(subnet_ids: [config[:subnet_id]]).subnets[0].vpc_id security_groups = [] filters = [config[:security_group_filter]].flatten filters.each do |sg_filter| # Built up rather than assigned, so that a filter carrying both a # name and a tag searches on both. Assigning meant the tag # replaced the name outright, silently widening the search to # whatever else carried that tag. criteria = [] if sg_filter[:name] criteria << { name: "group-name", values: [sg_filter[:name]] } end if sg_filter[:tag] criteria << { name: "tag:#{sg_filter[:tag]}", values: [sg_filter[:value]] } end # Refused rather than sent: describe_security_groups with no # filters returns every security group in the region, and all of # them were then attached to the instance. if criteria.empty? raise "A security_group_filter needs a `name` or a `tag`, but " \ "#{sg_filter.inspect} has neither." end criteria << { name: "vpc-id", values: [vpc_id] } security_group = client.describe_security_groups(filters: criteria).security_groups if security_group.any? security_group.each { |sg| security_groups.push(sg.group_id) } else raise "A Security Group matching the following filter could not be found:\n#{sg_filter}" end end config[:security_group_ids] = security_groups end i = { instance_type: config[:instance_type], ebs_optimized: config[:ebs_optimized], image_id: config[:image_id], key_name: config[:aws_ssh_key_id], subnet_id: config[:subnet_id], private_ip_address: config[:private_ip_address], min_count: 1, max_count: 1, } if config[:tags] && !config[:tags].empty? = config[:tags].map do |k, v| # we convert the value to a string because # nils should be passed as an empty String # and Integers need to be represented as Strings { key: k, value: v.to_s } end instance_tag_spec = { resource_type: "instance", tags: } volume_tag_spec = { resource_type: "volume", tags: } i[:tag_specifications] = [instance_tag_spec, volume_tag_spec] end unless config[:block_device_mappings].nil? || config[:block_device_mappings].empty? i[:block_device_mappings] = config[:block_device_mappings] end i[:security_group_ids] = Array(config[:security_group_ids]) if config[:security_group_ids] i[:metadata_options] = config[:metadata_options] if config[:metadata_options] i[:user_data] = prepared_user_data if prepared_user_data if config[:iam_profile_name] i[:iam_instance_profile] = { name: config[:iam_profile_name] } end unless config.fetch(:associate_public_ip, nil).nil? i[:network_interfaces] = [{ device_index: 0, associate_public_ip_address: config[:associate_public_ip], delete_on_termination: true, }] # If specifying `:network_interfaces` in the request, you must specify # network specific configs in the network_interfaces block and not at # the top level if config[:subnet_id] i[:network_interfaces][0][:subnet_id] = i.delete(:subnet_id) end if config[:private_ip_address] i[:network_interfaces][0][:private_ip_address] = i.delete(:private_ip_address) end if config[:security_group_ids] i[:network_interfaces][0][:groups] = i.delete(:security_group_ids) end if config[:associate_ipv6] i[:network_interfaces][0][:ipv_6_address_count] = 1 end end # A bare zone letter is a shorthand for that zone within the # configured region, so "b" in us-west-2 becomes "us-west-2b". availability_zone = config[:availability_zone] if availability_zone if /^[a-z]$/i.match?(availability_zone) availability_zone = "#{config[:region]}#{availability_zone}" end i[:placement] = { availability_zone: availability_zone.downcase } end tenancy = config[:tenancy] if tenancy if i.key?(:placement) i[:placement][:tenancy] = tenancy else i[:placement] = { tenancy: } end end placement = config[:placement] if placement unless i.key?(:placement) i[:placement] = {} end if placement[:affinity] i[:placement][:affinity] = placement[:affinity] end if placement[:availability_zone] i[:placement][:availability_zone] = placement[:availability_zone] end if placement[:group_id] && !placement[:group_name] i[:placement][:group_id] = placement[:group_id] end if placement[:group_name] && !placement[:group_id] i[:placement][:group_name] = placement[:group_name] end if placement[:host_id] i[:placement][:host_id] = placement[:host_id] end if placement[:host_resource_group_arn] i[:placement][:host_resource_group_arn] = placement[:host_resource_group_arn] end if placement[:partition_number] i[:placement][:partition_number] = placement[:partition_number] end if placement[:tenancy] i[:placement][:tenancy] = placement[:tenancy] end end # RunInstances calls this `license_specifications`. The driver option # is `licenses`, and the payload key used to match the option rather # than the API, which no EC2 API accepts. license_specifications = config[:licenses] if license_specifications i[:license_specifications] = license_specifications.map do |license| { license_configuration_arn: license[:license_configuration_arn] } end end unless config[:instance_initiated_shutdown_behavior].nil? || config[:instance_initiated_shutdown_behavior].empty? i[:instance_initiated_shutdown_behavior] = config[:instance_initiated_shutdown_behavior] end i end |
#prepared_user_data ⇒ String?
The user data script, base64 encoded as EC2 requires.
The configured value is treated as a file path when it names an
existing file, and as inline script content otherwise. Content
containing a null byte is always treated as inline, both because a
path cannot contain one and because File.file? would raise on it.
The result is memoized: the file is read once per driver, not once per call.
258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/kitchen/driver/aws/instance_generator.rb', line 258 def prepared_user_data # If user_data is a file reference, lets read it as such return nil if config[:user_data].nil? return @user_data if @user_data raw_user_data = config.fetch(:user_data) if !raw_user_data.include?("\0") && File.file?(raw_user_data) raw_user_data = File.read(raw_user_data) end @user_data = Base64.encode64(raw_user_data) end |