awspec 1.24.1 → 1.25.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }