awspec 1.24.1 → 1.25.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +69 -0
  3. data/.github/workflows/doc.yml +29 -0
  4. data/.rubocop.yml +47 -2
  5. data/Gemfile +2 -0
  6. data/README.md +2 -4
  7. data/Rakefile +0 -1
  8. data/awspec.gemspec +3 -5
  9. data/doc/_resource_types/cloudwatch_logs.md +9 -0
  10. data/doc/_resource_types/eks_nodegroup.md +53 -0
  11. data/doc/resource_types.md +62 -9
  12. data/lib/awspec/command/generate.rb +1 -1
  13. data/lib/awspec/generator/spec/cloudwatch_logs.rb +5 -1
  14. data/lib/awspec/generator/spec/elasticache.rb +43 -0
  15. data/lib/awspec/generator.rb +1 -0
  16. data/lib/awspec/helper/finder/ec2.rb +41 -16
  17. data/lib/awspec/helper/finder/ecs.rb +1 -1
  18. data/lib/awspec/helper/finder/subnet.rb +118 -20
  19. data/lib/awspec/helper/finder.rb +6 -0
  20. data/lib/awspec/helper/states.rb +16 -0
  21. data/lib/awspec/matcher/belong_to_subnets.rb +14 -0
  22. data/lib/awspec/matcher/have_metric_filter.rb +9 -0
  23. data/lib/awspec/matcher.rb +4 -0
  24. data/lib/awspec/setup.rb +1 -1
  25. data/lib/awspec/stub/cloudwatch_logs.rb +2 -1
  26. data/lib/awspec/stub/ec2_non_existing.rb +7 -0
  27. data/lib/awspec/stub/eks_nodegroup.rb +61 -1
  28. data/lib/awspec/stub/elasticsearch.rb +0 -1
  29. data/lib/awspec/stub/rds_db_parameter_group.rb +8 -0
  30. data/lib/awspec/stub/sns_topic.rb +15 -8
  31. data/lib/awspec/stub/sns_topic_error.rb +13 -0
  32. data/lib/awspec/type/cloudwatch_logs.rb +8 -3
  33. data/lib/awspec/type/ec2.rb +21 -16
  34. data/lib/awspec/type/eks_nodegroup.rb +105 -0
  35. data/lib/awspec/type/rds_db_parameter_group.rb +54 -0
  36. data/lib/awspec/type/s3_bucket.rb +1 -1
  37. data/lib/awspec/version.rb +1 -1
  38. metadata +26 -20
  39. data/.tachikoma.yml +0 -1
  40. data/.travis.yml +0 -23
@@ -51,7 +51,7 @@ module Awspec
51
51
  types_for_generate_all = %w(
52
52
  cloudwatch_alarm cloudwatch_event directconnect ebs efs
53
53
  elasticsearch iam_group iam_policy iam_role iam_user kms lambda
54
- acm cloudwatch_logs eip codebuild
54
+ acm cloudwatch_logs eip codebuild elasticache
55
55
  )
56
56
 
57
57
  types_for_generate_all.each do |type|
@@ -23,7 +23,11 @@ module Awspec::Generator
23
23
  metric_filters = select_all_cloudwatch_logs_metric_filter(log_group)
24
24
  metric_filter_lines = []
25
25
  metric_filters.each do |metric_filter|
26
- line = "it { should have_metric_filter('#{metric_filter.filter_name}') }"
26
+ line = "it { should have_metric_filter('#{metric_filter.filter_name}')"
27
+ unless metric_filter.filter_pattern.empty?
28
+ line += ".filter_pattern('#{metric_filter.filter_pattern}')"
29
+ end
30
+ line += ' }'
27
31
  metric_filter_lines.push(line)
28
32
  end
29
33
  metric_filter_lines
@@ -0,0 +1,43 @@
1
+ module Awspec::Generator
2
+ module Spec
3
+ class Elasticache
4
+ include Awspec::Helper::Finder
5
+ def generate_all
6
+ opt = {}
7
+ clusters = []
8
+ loop do
9
+ res = elasticache_client.describe_cache_clusters(opt)
10
+ clusters.push(*res.cache_clusters)
11
+ break if res.marker.nil?
12
+ opt = { marker: res.marker }
13
+ end
14
+ raise 'Not Found Cache Clusters' if clusters.empty?
15
+ ERB.new(cache_clusters_spec_template, nil, '-').result(binding).gsub(/^\n/, '')
16
+ end
17
+
18
+ def cache_clusters_spec_template
19
+ template = <<-'EOF'
20
+ <% clusters.each do |cluster| %>
21
+ describe elasticache('<%= cluster.cache_cluster_id %>') do
22
+ it { should exist }
23
+ it { should be_available }
24
+ it { should have_cache_parameter_group('<%= cluster.cache_parameter_group.cache_parameter_group_name %>') }
25
+ it { should belong_to_cache_subnet_group('<%= cluster.cache_subnet_group_name %>') }
26
+ <% unless cluster.replication_group_id.nil? %>
27
+ its(:replication_group_id) { should eq '<%= cluster.replication_group_id %>' }
28
+ <% end %>
29
+ its(:engine) { should eq '<%= cluster.engine %>' }
30
+ its(:engine_version) { should eq '<%= cluster.engine_version %>' }
31
+ its(:cache_node_type) { should eq '<%= cluster.cache_node_type %>' }
32
+ <% unless cluster.snapshot_retention_limit.nil? %>
33
+ its(:snapshot_retention_limit) { should eq <%= cluster.snapshot_retention_limit %> }
34
+ its(:snapshot_window) { should eq '<%= cluster.snapshot_window %>' }
35
+ <% end %>
36
+ end
37
+ <% end %>
38
+ EOF
39
+ template
40
+ end
41
+ end
42
+ end
43
+ end
@@ -27,6 +27,7 @@ require 'awspec/generator/spec/cloudwatch_logs'
27
27
  require 'awspec/generator/spec/alb'
28
28
  require 'awspec/generator/spec/nlb'
29
29
  require 'awspec/generator/spec/internet_gateway'
30
+ require 'awspec/generator/spec/elasticache'
30
31
  require 'awspec/generator/spec/elasticsearch'
31
32
  require 'awspec/generator/spec/eip'
32
33
  require 'awspec/generator/spec/rds_db_parameter_group'
@@ -3,23 +3,48 @@ module Awspec::Helper
3
3
  module Ec2
4
4
  def find_ec2(id)
5
5
  # instance_id or tag:Name
6
- begin
7
- res = ec2_client.describe_instances({
8
- instance_ids: [id]
9
- })
10
- rescue
11
- # Aws::EC2::Errors::InvalidInstanceIDMalformed
12
- # Aws::EC2::Errors::InvalidInstanceIDNotFound
13
- res = ec2_client.describe_instances({
14
- filters: [{ name: 'tag:Name', values: [id] }]
15
- })
16
- end
17
- # rubocop:enable Style/GuardClause
18
- if res.reservations.count == 1
19
- res.reservations.first.instances.single_resource(id)
20
- elsif res.reservations.count > 1
21
- raise Awspec::DuplicatedResourceTypeError, "Duplicate instances matching id or tag #{id}"
6
+
7
+ # First tries to search by using an educated guess, based on the
8
+ # references below:
9
+ # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/resource-ids.html
10
+ # https://docs.chef.io/inspec/resources/aws_ec2_instance/
11
+ # This should be faster then just first trying ID when the parameter is
12
+ # clearly not one
13
+
14
+ # https://medium.com/@Bakku1505/ruby-start-with-end-with-vs-regular-expressions-59728be0859e
15
+ if id.start_with?('i-') && id.length == 19 && id =~ /^i-[0-9a-f]/
16
+ begin
17
+ res = ec2_client.describe_instances({
18
+ instance_ids: [id]
19
+ })
20
+ rescue Aws::EC2::Errors::InvalidInstanceIDNotFound, Aws::EC2::Errors::InvalidInstanceIDMalformed => e
21
+ res = ec2_client.describe_instances({
22
+ filters: [{ name: 'tag:Name', values: [id] }]
23
+ })
24
+ end
25
+ else
26
+ begin
27
+ res = ec2_client.describe_instances({
28
+ filters: [{ name: 'tag:Name', values: [id] }]
29
+ })
30
+ rescue Aws::EC2::Errors::InvalidInstanceIDNotFound, Aws::EC2::Errors::InvalidInstanceIDMalformed => e
31
+ res = ec2_client.describe_instances({
32
+ instance_ids: [id]
33
+ })
34
+ if res.reservations.count > 1
35
+ STDERR.puts "Warning: '#{id}' unexpectedly identified as a valid instance ID during fallback search"
36
+ end
37
+ end
22
38
  end
39
+
40
+ return nil if res.reservations.count == 0
41
+ return res.reservations.first.instances.single_resource(id) if res.reservations.count == 1
42
+ raise Awspec::DuplicatedResourceTypeError, dup_ec2_instance(id) if res.reservations.count > 1
43
+ raise "Unexpected condition of having reservations = #{res.reservations.count}"
44
+ end
45
+
46
+ def dup_ec2_instance(id)
47
+ "Duplicate instances matching id or tag #{id}"
23
48
  end
24
49
 
25
50
  def find_ec2_attribute(id, attribute)
@@ -36,7 +36,7 @@ module Awspec::Helper
36
36
  # deprecated method
37
37
  def find_ecs_container_instances(cluster, container_instances)
38
38
  res = ecs_client.describe_container_instances(cluster: cluster, container_instances: container_instances)
39
- res.container_instances if res.container_instances
39
+ res.container_instances if res.container_instances # rubocop:disable Style/UnneededCondition
40
40
  end
41
41
 
42
42
  alias_method :list_ecs_container_instances, :select_ecs_container_instance_arn_by_cluster_name # deprecated method
@@ -1,29 +1,127 @@
1
+ require 'singleton'
2
+
1
3
  module Awspec::Helper
2
4
  module Finder
3
5
  module Subnet
4
- def find_subnet(subnet_id)
5
- res = ec2_client.describe_subnets({
6
- filters: [{ name: 'subnet-id', values: [subnet_id] }]
7
- })
8
- resource = res.subnets.single_resource(subnet_id)
9
- return resource if resource
10
- res = ec2_client.describe_subnets({
11
- filters: [{ name: 'tag:Name', values: [subnet_id] }]
12
- })
13
- resource = res.subnets.single_resource(subnet_id)
14
- return resource if resource
15
- res = ec2_client.describe_subnets({
16
- filters: [{ name: 'cidrBlock', values: [subnet_id] }]
17
- })
18
- res.subnets.single_resource(subnet_id)
6
+ # Implements in-memory cache for +AWS::Ec2::Client+ +describe_subnets+
7
+ # method.
8
+
9
+ # == Usage
10
+ # Includes {Singleton}[https://ruby-doc.org/stdlib-2.7.3/libdoc/singleton/rdoc/index.html]
11
+ # module, so use +instance+ instead of +new+ to get a instance.
12
+ #
13
+ # It is intended to be used internally by the +find_subnet+ function only.
14
+ #
15
+ # Many of the methods expect a symbol to search through the cache to
16
+ # avoid having to call +to_sym+ multiple times.
17
+
18
+ class SubnetCache
19
+ include Singleton
20
+
21
+ def initialize # :nodoc:
22
+ @by_tag_name = {}
23
+ @by_cidr = {}
24
+ @subnet_ids = {}
25
+ @ip_matcher = Regexp.new('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$')
26
+ end
27
+
28
+ # Add a mapping of a CIDR to the respective subnet ID
29
+ def add_by_cidr(cidr, subnet_id)
30
+ key_sym = cidr.to_sym
31
+ @by_cidr[key_sym] = subnet_id.to_sym unless @by_cidr.key?(key_sym)
32
+ end
33
+
34
+ # Add a mapping of a tag to the respective subnet ID
35
+ def add_by_tag(tag, subnet_id)
36
+ key_sym = tag.to_sym
37
+ @by_tag_name[key_sym] = subnet_id.to_sym unless @by_tag_name.key?(key_sym)
38
+ end
39
+
40
+ # Add a +Aws::EC2::Types::Subnet+ instance to the cache, mapping it's ID
41
+ # to the instance itself.
42
+ def add_subnet(subnet)
43
+ key_sym = subnet.subnet_id.to_sym
44
+ @subnet_ids[key_sym] = subnet unless @subnet_ids.key?(key_sym)
45
+ end
46
+
47
+ # Check if a subnet ID (as a symbol) exists in the cache.
48
+ def has_subnet?(subnet_id_symbol)
49
+ @subnet_ids.key?(subnet_id_symbol)
50
+ end
51
+
52
+ # Return a +Aws::EC2::Types::Subnet+ that matches the given CIDR.
53
+ def subnet_by_cidr(cidr_symbol)
54
+ @subnet_ids[@by_cidr[cidr_symbol]]
55
+ end
56
+
57
+ # Return a +Aws::EC2::Types::Subnet+ that matches the given tag.
58
+ def subnet_by_tag(tag_symbol)
59
+ @subnet_ids[@by_tag_name[tag_symbol]]
60
+ end
61
+
62
+ # Return a +Aws::EC2::Types::Subnet+ that matches the given subnet ID.
63
+ def subnet_by_id(subnet_id_symbol)
64
+ @subnet_ids[subnet_id_symbol]
65
+ end
66
+
67
+ # Check if a given string looks like a IPv4 CIDR.
68
+ def is_cidr?(subnet_id)
69
+ @ip_matcher.match(subnet_id)
70
+ end
71
+
72
+ # Check if the cache was already initialized or not.
73
+ def empty?
74
+ @subnet_ids.empty?
75
+ end
76
+
77
+ # Return the cache as a string.
78
+ def to_s
79
+ "by tag name: #{@by_tag_name}, by CIDR: #{@by_cidr}"
80
+ end
19
81
  end
20
82
 
21
- def select_subnet_by_vpc_id(vpc_id)
22
- res = ec2_client.describe_subnets({
23
- filters: [{ name: 'vpc-id', values: [vpc_id] }]
24
- })
25
- res.subnets
83
+ # Try to locate a +Aws::EC2::Types::Subnet+ with a given subnet ID.
84
+ #
85
+ # A subnet ID might be multiple things, like the
86
+ # +Aws::EC2::Types::Subnet.subnet_id+, or a IPv4 CIDR or the value for the
87
+ # +Name+ tag associated with the subnet.
88
+ #
89
+ # Returns a instance of +Aws::EC2::Types::Subnet+ or +nil+.
90
+ def find_subnet(subnet_id)
91
+ cache = SubnetCache.instance
92
+
93
+ if cache.empty?
94
+ res = ec2_client.describe_subnets
95
+
96
+ res.subnets.each do |sub|
97
+ cache.add_by_cidr(sub.cidr_block, sub.subnet_id)
98
+ cache.add_subnet(sub)
99
+ next if sub.tags.empty?
100
+
101
+ sub.tags.each do |tag|
102
+ if tag[:key].eql?('Name')
103
+ cache.add_by_tag(tag[:value], sub.subnet_id)
104
+ break
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ id_key = subnet_id.to_sym
111
+ return cache.subnet_by_id(id_key) if subnet_id.start_with?('subnet-') && cache.has_subnet?(id_key)
112
+ return cache.subnet_by_cidr(id_key) if cache.is_cidr?(subnet_id) && cache.by_cidr.key?(id_key)
113
+ cache.subnet_by_tag(id_key)
26
114
  end
27
115
  end
116
+
117
+ # Search for the subnets associated with a given VPC ID.
118
+ #
119
+ # Returns an array of +Aws::EC2::Types::Subnet+ instances.
120
+ def select_subnet_by_vpc_id(vpc_id)
121
+ res = ec2_client.describe_subnets({
122
+ filters: [{ name: 'vpc-id', values: [vpc_id] }]
123
+ })
124
+ res.subnets
125
+ end
28
126
  end
29
127
  end
@@ -166,6 +166,12 @@ module Awspec::Helper
166
166
  http_wire_trace: ENV['http_wire_trace'] || false
167
167
  }
168
168
 
169
+ check_configuration = ENV['DISABLE_AWS_CLIENT_CHECK'] != 'true' if ENV.key?('DISABLE_AWS_CLIENT_CHECK')
170
+
171
+ # define_method below will "hide" any exception that comes from bad
172
+ # setup of AWS client, so let's try first to create a instance
173
+ Awsecrets.load if check_configuration
174
+
169
175
  CLIENTS.each do |method_name, client|
170
176
  define_method method_name do
171
177
  unless self.methods.include? "@#{method_name}"
@@ -0,0 +1,16 @@
1
+ module Awspec::Helper
2
+ module States
3
+ EC2_STATES = %w(pending running shutting-down terminated stopping stopped)
4
+
5
+ def self.ec2_states_checks
6
+ Enumerator.new do |yielder|
7
+ n = 0
8
+ while n < EC2_STATES.size
9
+ method_name = EC2_STATES[n].tr('-', '_') + '?'
10
+ yielder.yield(method_name, EC2_STATES[n])
11
+ n += 1
12
+ end
13
+ end.lazy
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ RSpec::Matchers.define :belong_to_subnets do |*subnets|
2
+ match do |nodegroup|
3
+ superset = Set.new(subnets)
4
+ nodegroup.subnets.subset?(superset)
5
+ end
6
+ failure_message { |nodegroup| super() + failure_reason(nodegroup) }
7
+ failure_message_when_negated { |nodegroup| super() + failure_reason(nodegroup) }
8
+ end
9
+
10
+ private
11
+
12
+ def failure_reason(nodegroup)
13
+ ", but the nodes are allocated to the subnets #{nodegroup.subnets.to_a}"
14
+ end
@@ -0,0 +1,9 @@
1
+ RSpec::Matchers.define :have_metric_filter do |filter_name|
2
+ match do |log_group_name|
3
+ log_group_name.has_metric_filter?(filter_name, @pattern)
4
+ end
5
+
6
+ chain :filter_pattern do |pattern|
7
+ @pattern = pattern
8
+ end
9
+ end
@@ -4,6 +4,9 @@ require 'awspec/matcher/belong_to_subnet'
4
4
  require 'awspec/matcher/have_tag'
5
5
  require 'awspec/matcher/have_network_interface'
6
6
 
7
+ # EKS
8
+ require 'awspec/matcher/belong_to_subnets'
9
+
7
10
  # RDS
8
11
  require 'awspec/matcher/belong_to_db_subnet_group'
9
12
  require 'awspec/matcher/have_db_parameter_group'
@@ -53,6 +56,7 @@ require 'awspec/matcher/have_rule'
53
56
 
54
57
  # CloudWatch Logs
55
58
  require 'awspec/matcher/have_subscription_filter'
59
+ require 'awspec/matcher/have_metric_filter'
56
60
 
57
61
  # DynamoDB
58
62
  require 'awspec/matcher/have_attribute_definition'
data/lib/awspec/setup.rb CHANGED
@@ -18,7 +18,7 @@ EOF
18
18
  dir = 'spec'
19
19
  if File.exist? dir
20
20
  unless File.directory? dir
21
- $stderr.puts '!! #{dir} already exists and is not a directory'
21
+ $stderr.puts "!! #{dir} already exists and is not a directory"
22
22
  end
23
23
  else
24
24
  FileUtils.mkdir dir
@@ -18,7 +18,8 @@ Aws.config[:cloudwatchlogs] = {
18
18
  describe_metric_filters: {
19
19
  metric_filters: [
20
20
  {
21
- filter_name: 'my-cloudwatch-logs-metric-filter'
21
+ filter_name: 'my-cloudwatch-logs-metric-filter',
22
+ filter_pattern: '[date, error]'
22
23
  }
23
24
  ]
24
25
  },
@@ -0,0 +1,7 @@
1
+ Aws.config[:ec2] = {
2
+ stub_responses: {
3
+ describe_instances: {
4
+ reservations: []
5
+ }
6
+ }
7
+ }
@@ -9,8 +9,68 @@ Aws.config[:eks] = {
9
9
  nodegroup_arn: 'arn:aws:eks:us-west-2:012345678910:nodegroup/my-cluster/my-nodegroup/08bd000a',
10
10
  created_at: Time.parse('2018-10-28 00:23:32 -0400'),
11
11
  node_role: 'arn:aws:iam::012345678910:role/eks-nodegroup-role',
12
- status: 'ACTIVE'
12
+ status: 'ACTIVE',
13
+ scaling_config: { min_size: 1, desired_size: 2, max_size: 3 }
13
14
  }
14
15
  }
15
16
  }
16
17
  }
18
+
19
+ Aws.config[:ec2] = {
20
+ stub_responses: {
21
+ describe_instances: {
22
+ reservations: [
23
+ {
24
+ instances: [
25
+ {
26
+ instance_id: 'i-ec12345a',
27
+ image_id: 'ami-abc12def',
28
+ vpc_id: 'vpc-ab123cde',
29
+ subnet_id: 'subnet-1234a567',
30
+ public_ip_address: '123.0.456.789',
31
+ private_ip_address: '10.0.1.1',
32
+ instance_type: 't2.small',
33
+ state: {
34
+ name: 'running'
35
+ },
36
+ security_groups: [
37
+ {
38
+ group_id: 'sg-1a2b3cd4',
39
+ group_name: 'my-security-group-name'
40
+ }
41
+ ],
42
+ iam_instance_profile: {
43
+ arn: 'arn:aws:iam::123456789012:instance-profile/Ec2IamProfileName',
44
+ id: 'ABCDEFGHIJKLMNOPQRSTU'
45
+ },
46
+ block_device_mappings: [
47
+ {
48
+ device_name: '/dev/sda',
49
+ ebs: {
50
+ volume_id: 'vol-123a123b'
51
+ }
52
+ }
53
+ ],
54
+ network_interfaces: [
55
+ {
56
+ network_interface_id: 'eni-12ab3cde',
57
+ subnet_id: 'subnet-1234a567',
58
+ vpc_id: 'vpc-ab123cde',
59
+ attachment: {
60
+ device_index: 1
61
+ }
62
+ }
63
+ ],
64
+ tags: [
65
+ {
66
+ key: 'Name',
67
+ value: 'my-ec2'
68
+ }
69
+ ]
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ }