awspec 1.24.2 → 1.25.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,29 +1,132 @@
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
+ # Check if a IPv4 CIDR (as a symbol) exists in the cache.
53
+ def has_cidr?(cidr_symbol)
54
+ @by_cidr.key?(cidr_symbol)
55
+ end
56
+
57
+ # Return a +Aws::EC2::Types::Subnet+ that matches the given CIDR.
58
+ def subnet_by_cidr(cidr_symbol)
59
+ @subnet_ids[@by_cidr[cidr_symbol]]
60
+ end
61
+
62
+ # Return a +Aws::EC2::Types::Subnet+ that matches the given tag.
63
+ def subnet_by_tag(tag_symbol)
64
+ @subnet_ids[@by_tag_name[tag_symbol]]
65
+ end
66
+
67
+ # Return a +Aws::EC2::Types::Subnet+ that matches the given subnet ID.
68
+ def subnet_by_id(subnet_id_symbol)
69
+ @subnet_ids[subnet_id_symbol]
70
+ end
71
+
72
+ # Check if a given string looks like a IPv4 CIDR.
73
+ def is_cidr?(subnet_id)
74
+ @ip_matcher.match(subnet_id)
75
+ end
76
+
77
+ # Check if the cache was already initialized or not.
78
+ def empty?
79
+ @subnet_ids.empty?
80
+ end
81
+
82
+ # Return the cache as a string.
83
+ def to_s
84
+ "by tag name: #{@by_tag_name}, by CIDR: #{@by_cidr}"
85
+ end
19
86
  end
20
87
 
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
88
+ # Try to locate a +Aws::EC2::Types::Subnet+ with a given subnet ID.
89
+ #
90
+ # A subnet ID might be multiple things, like the
91
+ # +Aws::EC2::Types::Subnet.subnet_id+, or a IPv4 CIDR or the value for the
92
+ # +Name+ tag associated with the subnet.
93
+ #
94
+ # Returns a instance of +Aws::EC2::Types::Subnet+ or +nil+.
95
+ def find_subnet(subnet_id)
96
+ cache = SubnetCache.instance
97
+
98
+ if cache.empty?
99
+ res = ec2_client.describe_subnets
100
+
101
+ res.subnets.each do |sub|
102
+ cache.add_by_cidr(sub.cidr_block, sub.subnet_id)
103
+ cache.add_subnet(sub)
104
+ next if sub.tags.empty?
105
+
106
+ sub.tags.each do |tag|
107
+ if tag[:key].eql?('Name')
108
+ cache.add_by_tag(tag[:value], sub.subnet_id)
109
+ break
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ id_key = subnet_id.to_sym
116
+ return cache.subnet_by_id(id_key) if subnet_id.start_with?('subnet-') && cache.has_subnet?(id_key)
117
+ return cache.subnet_by_cidr(id_key) if cache.is_cidr?(subnet_id) && cache.has_cidr?(id_key)
118
+ cache.subnet_by_tag(id_key)
26
119
  end
27
120
  end
121
+
122
+ # Search for the subnets associated with a given VPC ID.
123
+ #
124
+ # Returns an array of +Aws::EC2::Types::Subnet+ instances.
125
+ def select_subnet_by_vpc_id(vpc_id)
126
+ res = ec2_client.describe_subnets({
127
+ filters: [{ name: 'vpc-id', values: [vpc_id] }]
128
+ })
129
+ res.subnets
130
+ end
28
131
  end
29
132
  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
  },
@@ -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
+ }
@@ -1,4 +1,3 @@
1
-
2
1
  Aws.config[:elasticsearchservice] = {
3
2
  stub_responses: {
4
3
  list_domain_names: {
@@ -13,6 +13,14 @@ Aws.config[:rds] = {
13
13
  {
14
14
  parameter_name: 'max_allowed_packet',
15
15
  parameter_value: '16777216'
16
+ },
17
+ {
18
+ parameter_name: 'rds.logical_replication',
19
+ parameter_value: '1'
20
+ },
21
+ {
22
+ parameter_name: 'rds.accepted_password_auth_method',
23
+ parameter_value: 'md5+scram'
16
24
  }
17
25
  ]
18
26
  }
@@ -1,26 +1,33 @@
1
+ OWNER = '123456789'
2
+ REGION = 'us-east-1'
3
+ TOPIC_ARN = "arn:aws:sns:#{REGION}:#{OWNER}:foobar"
4
+ DISPLAY_NAME = 'Useless'
5
+ SUBSCRIBED = "arn:aws:sns:#{REGION}:#{OWNER}:Foobar:3dbf4999-b3e2-4345-bd11-c34c9784ecca"
6
+ ENDPOINT = "arn:aws:lambda:#{REGION}:#{OWNER}:function:foobar"
7
+
1
8
  Aws.config[:sns] = {
2
9
  stub_responses: {
3
10
  get_topic_attributes: {
4
11
  attributes: {
5
12
  # rubocop:disable LineLength
6
- 'Policy' => '{\"Version\":\"2008-10-17\",\"Id\":\"__default_policy_ID\",\"Statement\":[{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:GetTopicAttributes\",\"SNS:SetTopicAttributes\",\"SNS:AddPermission\",\"SNS:RemovePermission\",\"SNS:DeleteTopic\",\"SNS:Subscribe\",\"SNS:ListSubscriptionsByTopic\",\"SNS:Publish\",\"SNS:Receive\"],\"Resource\":\"arn:aws:sns:us-east-1:123456789:foobar-lambda-sample\",\"Condition\":{\"StringEquals\":{\"AWS:SourceOwner\":\"123456789\"}}}]}',
7
- 'Owner' => '123456789',
13
+ 'Policy' => "{\"Version\":\"2008-10-17\",\"Id\":\"__default_policy_ID\",\"Statement\":[{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:GetTopicAttributes\",\"SNS:SetTopicAttributes\",\"SNS:AddPermission\",\"SNS:RemovePermission\",\"SNS:DeleteTopic\",\"SNS:Subscribe\",\"SNS:ListSubscriptionsByTopic\",\"SNS:Publish\",\"SNS:Receive\"],\"Resource\":\"arn:aws:sns:#{REGION}:#{OWNER}:foobar-lambda-sample\",\"Condition\":{\"StringEquals\":{\"AWS:SourceOwner\":\"#{OWNER}\"}}}]}",
14
+ 'Owner' => OWNER,
8
15
  'SubscriptionsPending' => '0',
9
- 'TopicArn' => 'arn:aws:sns:us-east-1:123456789:foobar',
16
+ 'TopicArn' => TOPIC_ARN,
10
17
  'EffectiveDeliveryPolicy' => '{\"http\":{\"defaultHealthyRetryPolicy\":{\"minDelayTarget\":20,\"maxDelayTarget\":20,\"numRetries\":3,\"numMaxDelayRetries\":0,\"numNoDelayRetries\":0,\"numMinDelayRetries\":0,\"backoffFunction\":\"linear\"},\"disableSubscriptionOverrides\":false}}',
11
18
  'SubscriptionsConfirmed' => '1',
12
- 'DisplayName' => 'Useless',
19
+ 'DisplayName' => DISPLAY_NAME,
13
20
  'SubscriptionsDeleted' => '0'
14
21
  }
15
22
  },
16
23
  list_subscriptions_by_topic: {
17
24
  subscriptions: [
18
25
  {
19
- subscription_arn: 'arn:aws:sns:us-east-1:123456789:Foobar:3dbf4999-b3e2-4345-bd11-c34c9784ecca',
20
- owner: '123456789',
26
+ subscription_arn: SUBSCRIBED,
27
+ owner: OWNER,
21
28
  protocol: 'lambda',
22
- endpoint: 'arn:aws:lambda:us-east-1:123456789:function:foobar',
23
- topic_arn: 'arn:aws:sns:us-east-1:123456789:foobar'
29
+ endpoint: ENDPOINT,
30
+ topic_arn: TOPIC_ARN
24
31
  }
25
32
  ],
26
33
  next_token: nil
@@ -0,0 +1,13 @@
1
+ OWNER = '123456789'
2
+ REGION = 'us-east-1'
3
+ TOPIC_ARN = "arn:aws:sns:#{REGION}:#{OWNER}:invalid"
4
+ TOPIC_SUBS_ARN = "arn:aws:sns:us-east-1:#{OWNER}:Foobar:3dbf4999-b3e2-4345-bd11-c34c9784ecca"
5
+
6
+ Aws.config[:sns] = {
7
+ stub_responses: {
8
+ get_topic_attributes: Aws::SNS::Errors::NotFound.new(
9
+ TOPIC_ARN, 'no such topic'),
10
+ list_subscriptions_by_topic: Aws::SNS::Errors::NotFound.new(
11
+ TOPIC_SUBS_ARN, 'no such topic')
12
+ }
13
+ }
@@ -13,9 +13,14 @@ module Awspec::Type
13
13
  return true if ret == stream_name
14
14
  end
15
15
 
16
- def has_metric_filter?(filter_name)
17
- ret = find_cloudwatch_logs_metric_fileter_by_log_group_name(@id, filter_name).filter_name
18
- return true if ret == filter_name
16
+ def has_metric_filter?(filter_name, pattern = nil)
17
+ ret = find_cloudwatch_logs_metric_fileter_by_log_group_name(@id, filter_name)
18
+ if pattern.nil?
19
+ return true if ret.filter_name == filter_name
20
+ else
21
+ return false unless ret.filter_pattern == pattern
22
+ end
23
+ return true if ret.filter_name == filter_name
19
24
  end
20
25
 
21
26
  def has_subscription_filter?(filter_name, pattern = nil)
@@ -1,10 +1,47 @@
1
+ require 'set'
2
+
1
3
  module Awspec::Type
4
+ class EksNodeEC2
5
+ attr_reader :state, :subnet_id, :sec_groups
6
+
7
+ def initialize(id, state, subnet_id, sec_groups)
8
+ @id = id
9
+ @state = state
10
+ @subnet_id = subnet_id
11
+ @sec_groups = sec_groups
12
+ end
13
+
14
+ def to_s
15
+ "ID: #{@id}, State: #{@state}, Subnet ID: #{@subnet_id}, Security Groups: #{@sec_groups}"
16
+ end
17
+ end
18
+
19
+ class EksNodeSecGroup
20
+ attr_reader :name, :id
21
+
22
+ def initialize(id, name)
23
+ @id = id
24
+ @name = name
25
+ end
26
+
27
+ def to_s
28
+ "ID: #{@id}, Name: #{@name}"
29
+ end
30
+ end
31
+
2
32
  class EksNodegroup < ResourceBase
33
+ # the tags below are standard for EKS node groups instances
34
+ EKS_CLUSTER_TAG = 'tag:eks:cluster-name'
35
+ EKS_NODEGROUP_TAG = 'tag:eks:nodegroup-name'
36
+
3
37
  attr_accessor :cluster
4
38
 
5
39
  def initialize(group_name)
6
40
  super
7
41
  @group_name = group_name
42
+ @ec2_instances = []
43
+ @sec_groups = Set.new
44
+ @sec_groups_ids = Set.new
8
45
  end
9
46
 
10
47
  def resource_via_client
@@ -19,6 +56,39 @@ module Awspec::Type
19
56
  @cluster || 'default'
20
57
  end
21
58
 
59
+ def subnets
60
+ ec2_instances = find_nodes
61
+ Set.new(ec2_instances.map { |ec2| ec2.subnet_id })
62
+ end
63
+
64
+ def has_security_group?(sec_group)
65
+ if @sec_groups.empty? || @sec_groups_ids.empty?
66
+ ec2_instances = find_nodes
67
+
68
+ ec2_instances.each do |ec2|
69
+ ec2.sec_groups.each do |sg|
70
+ @sec_groups.add(sg.name)
71
+ @sec_groups_ids.add(sg.id)
72
+ end
73
+ end
74
+ end
75
+
76
+ @sec_groups.member?(sec_group) || @sec_groups_ids.member?(sec_group)
77
+ end
78
+
79
+ def ready?
80
+ min_expected = resource_via_client.scaling_config.min_size
81
+ ec2_instances = find_nodes
82
+ running_counter = 0
83
+
84
+ ec2_instances.each do |ec2|
85
+ running_counter += 1 if ec2.state.eql?('running')
86
+ break if running_counter == min_expected
87
+ end
88
+
89
+ running_counter >= min_expected
90
+ end
91
+
22
92
  STATES = %w(ACTIVE INACTIVE)
23
93
 
24
94
  STATES.each do |state|
@@ -26,5 +96,40 @@ module Awspec::Type
26
96
  resource_via_client.status == state
27
97
  end
28
98
  end
99
+
100
+ private
101
+
102
+ def find_nodes
103
+ return @ec2_instances unless @ec2_instances.empty?
104
+
105
+ result = ec2_client.describe_instances(
106
+ {
107
+ filters: [
108
+ { name: EKS_CLUSTER_TAG, values: [cluster] },
109
+ { name: EKS_NODEGROUP_TAG, values: [@group_name] }
110
+ ]
111
+ }
112
+ )
113
+ result.reservations.each do |reservation|
114
+ reservation.instances.each do |instance|
115
+ sec_groups = []
116
+
117
+ instance.security_groups.map do |sg|
118
+ sec_groups.push(EksNodeSecGroup.new(sg.group_id, sg.group_name))
119
+ end
120
+
121
+ @ec2_instances.push(
122
+ EksNodeEC2.new(
123
+ instance.instance_id,
124
+ instance.state.name,
125
+ instance.subnet_id,
126
+ sec_groups
127
+ )
128
+ )
129
+ end
130
+ end
131
+
132
+ @ec2_instances
133
+ end
29
134
  end
30
135
  end
@@ -1,4 +1,39 @@
1
1
  module Awspec::Type
2
+ class InvalidRdsDbParameter < StandardError
3
+ ##
4
+ # Overrides the superclass initialize method to include more information
5
+ # and default error message.
6
+ # Expected parameters:
7
+ # - parameter_name: the name of the parameter.
8
+
9
+ def initialize(parameter_name)
10
+ @param_name = parameter_name
11
+ message = "There is no such parameter \"rds.#{parameter_name}\""
12
+ super message
13
+ end
14
+ end
15
+
16
+ class RdsDBParameters
17
+ ##
18
+ # Thanks to AWS for creating parameters names like
19
+ # 'rds.accepted_password_auth_method', which would be caught as method 'rds'
20
+ # by method_missing in RdsDbParameterGroup class, this class was created
21
+ # See https://github.com/k1LoW/awspec/issues/527 for more details
22
+ def initialize(params)
23
+ @params = params
24
+ end
25
+
26
+ def to_s
27
+ return "RdsDBParameters = #{@params}"
28
+ end
29
+
30
+ def method_missing(name)
31
+ param_name = name.to_sym
32
+ return @params[param_name] if @params.include?(param_name)
33
+ raise InvalidRdsDbParameter, name
34
+ end
35
+ end
36
+
2
37
  class RdsDbParameterGroup < ResourceBase
3
38
  def resource_via_client
4
39
  return @resource_via_client if @resource_via_client
@@ -11,11 +46,30 @@ module Awspec::Type
11
46
 
12
47
  def method_missing(name)
13
48
  param_name = name.to_s
49
+ return create_rds_params if param_name == 'rds'
50
+
14
51
  if resource_via_client.include?(param_name)
15
52
  resource_via_client[param_name].to_s
16
53
  else
17
54
  super
18
55
  end
19
56
  end
57
+
58
+ private
59
+
60
+ def create_rds_params
61
+ return @rds_params if @rds_params
62
+
63
+ rds_params_keys = resource_via_client.keys.select { |key| key.to_s.start_with?('rds.') }
64
+ rds_params = {}
65
+
66
+ rds_params_keys.each do |key|
67
+ new_key = key.split('.')[-1]
68
+ rds_params[new_key.to_sym] = resource_via_client[key]
69
+ end
70
+
71
+ @rds_params = RdsDBParameters.new(rds_params)
72
+ @rds_params
73
+ end
20
74
  end
21
75
  end
@@ -108,8 +108,8 @@ module Awspec::Type
108
108
  if value.is_a?(Array)
109
109
  return false if r[key].map(&:to_h) != value
110
110
  end
111
- true
112
111
  end
112
+ true
113
113
  end
114
114
  end
115
115
 
@@ -1,3 +1,3 @@
1
1
  module Awspec
2
- VERSION = '1.24.2'
2
+ VERSION = '1.25.1'
3
3
  end