stack_master 1.6.0-x64-mingw32
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 +7 -0
- data/README.md +548 -0
- data/bin/stack_master +17 -0
- data/lib/stack_master.rb +159 -0
- data/lib/stack_master/aws_driver/cloud_formation.rb +41 -0
- data/lib/stack_master/aws_driver/s3.rb +68 -0
- data/lib/stack_master/change_set.rb +109 -0
- data/lib/stack_master/cli.rb +208 -0
- data/lib/stack_master/command.rb +57 -0
- data/lib/stack_master/commands/apply.rb +221 -0
- data/lib/stack_master/commands/delete.rb +53 -0
- data/lib/stack_master/commands/diff.rb +31 -0
- data/lib/stack_master/commands/events.rb +39 -0
- data/lib/stack_master/commands/init.rb +111 -0
- data/lib/stack_master/commands/list_stacks.rb +20 -0
- data/lib/stack_master/commands/outputs.rb +31 -0
- data/lib/stack_master/commands/resources.rb +33 -0
- data/lib/stack_master/commands/status.rb +46 -0
- data/lib/stack_master/commands/terminal_helper.rb +28 -0
- data/lib/stack_master/commands/validate.rb +17 -0
- data/lib/stack_master/config.rb +133 -0
- data/lib/stack_master/ctrl_c.rb +4 -0
- data/lib/stack_master/paged_response_accumulator.rb +29 -0
- data/lib/stack_master/parameter_loader.rb +49 -0
- data/lib/stack_master/parameter_resolver.rb +98 -0
- data/lib/stack_master/parameter_resolvers/ami_finder.rb +36 -0
- data/lib/stack_master/parameter_resolvers/env.rb +18 -0
- data/lib/stack_master/parameter_resolvers/latest_ami.rb +19 -0
- data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +18 -0
- data/lib/stack_master/parameter_resolvers/parameter_store.rb +31 -0
- data/lib/stack_master/parameter_resolvers/secret.rb +52 -0
- data/lib/stack_master/parameter_resolvers/security_group.rb +22 -0
- data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +31 -0
- data/lib/stack_master/parameter_resolvers/stack_output.rb +76 -0
- data/lib/stack_master/prompter.rb +21 -0
- data/lib/stack_master/resolver_array.rb +35 -0
- data/lib/stack_master/security_group_finder.rb +28 -0
- data/lib/stack_master/sns_topic_finder.rb +26 -0
- data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +35 -0
- data/lib/stack_master/sparkle_formation/compile_time/allowed_values_validator.rb +37 -0
- data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +33 -0
- data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +32 -0
- data/lib/stack_master/sparkle_formation/compile_time/max_length_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/max_size_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/min_length_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/min_size_validator.rb +36 -0
- data/lib/stack_master/sparkle_formation/compile_time/number_validator.rb +35 -0
- data/lib/stack_master/sparkle_formation/compile_time/parameters_validator.rb +27 -0
- data/lib/stack_master/sparkle_formation/compile_time/state_builder.rb +32 -0
- data/lib/stack_master/sparkle_formation/compile_time/string_validator.rb +33 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +40 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +40 -0
- data/lib/stack_master/sparkle_formation/compile_time/value_validator_factory.rb +41 -0
- data/lib/stack_master/sparkle_formation/template_file.rb +115 -0
- data/lib/stack_master/stack.rb +105 -0
- data/lib/stack_master/stack_definition.rb +103 -0
- data/lib/stack_master/stack_differ.rb +111 -0
- data/lib/stack_master/stack_events/fetcher.rb +38 -0
- data/lib/stack_master/stack_events/presenter.rb +27 -0
- data/lib/stack_master/stack_events/streamer.rb +68 -0
- data/lib/stack_master/stack_states.rb +34 -0
- data/lib/stack_master/stack_status.rb +61 -0
- data/lib/stack_master/template_compiler.rb +30 -0
- data/lib/stack_master/template_compilers/cfndsl.rb +13 -0
- data/lib/stack_master/template_compilers/json.rb +22 -0
- data/lib/stack_master/template_compilers/sparkle_formation.rb +71 -0
- data/lib/stack_master/template_compilers/yaml.rb +14 -0
- data/lib/stack_master/template_utils.rb +31 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +193 -0
- data/lib/stack_master/test_driver/s3.rb +34 -0
- data/lib/stack_master/testing.rb +9 -0
- data/lib/stack_master/utils.rb +50 -0
- data/lib/stack_master/validator.rb +33 -0
- data/lib/stack_master/version.rb +3 -0
- metadata +457 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class AmiFinder
|
4
|
+
def initialize(region)
|
5
|
+
@region = region
|
6
|
+
end
|
7
|
+
|
8
|
+
def build_filters_from_string(value, prefix = nil)
|
9
|
+
filters = value.split(',').map do |name_with_value|
|
10
|
+
name, value = name_with_value.strip.split('=')
|
11
|
+
name = prefix ? "#{prefix}:#{name}" : name
|
12
|
+
{ name: name, values: [value] }
|
13
|
+
end
|
14
|
+
filters
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_filters_from_hash(hash)
|
18
|
+
hash.map { |key, value| {name: key, values: Array(value.to_s)}}
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_latest_ami(filters, owners = ['self'])
|
22
|
+
images = ec2.describe_images(owners: owners, filters: filters).images
|
23
|
+
sorted_images = images.sort do |a, b|
|
24
|
+
Time.parse(a.creation_date) <=> Time.parse(b.creation_date)
|
25
|
+
end
|
26
|
+
sorted_images.last
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def ec2
|
32
|
+
@ec2 ||= Aws::EC2::Client.new(region: @region)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class Env < Resolver
|
4
|
+
|
5
|
+
def initialize(config, stack_definition)
|
6
|
+
@config = config
|
7
|
+
@stack_definition = stack_definition
|
8
|
+
end
|
9
|
+
|
10
|
+
def resolve(value)
|
11
|
+
environment_variable = ENV[value]
|
12
|
+
raise ArgumentError, "The environment variable #{value} is not set" if environment_variable.nil?
|
13
|
+
environment_variable
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class LatestAmi < Resolver
|
4
|
+
array_resolver class_name: 'LatestAmis'
|
5
|
+
|
6
|
+
def initialize(config, stack_definition)
|
7
|
+
@config = config
|
8
|
+
@stack_definition = stack_definition
|
9
|
+
@ami_finder = AmiFinder.new(@stack_definition.region)
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(value)
|
13
|
+
owners = Array(value.fetch('owners', 'self').to_s)
|
14
|
+
filters = @ami_finder.build_filters_from_hash(value.fetch('filters'))
|
15
|
+
@ami_finder.find_latest_ami(filters, owners).try(:image_id)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class LatestAmiByTags < Resolver
|
4
|
+
array_resolver class_name: 'LatestAmisByTags'
|
5
|
+
|
6
|
+
def initialize(config, stack_definition)
|
7
|
+
@config = config
|
8
|
+
@stack_definition = stack_definition
|
9
|
+
@ami_finder = AmiFinder.new(@stack_definition.region)
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(value)
|
13
|
+
filters = @ami_finder.build_filters_from_string(value, prefix = "tag")
|
14
|
+
@ami_finder.find_latest_ami(filters).try(:image_id)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class ParameterStore < Resolver
|
4
|
+
|
5
|
+
ParameterNotFound = Class.new(StandardError)
|
6
|
+
|
7
|
+
def initialize(config, stack_definition)
|
8
|
+
@config = config
|
9
|
+
@stack_definition = stack_definition
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(value)
|
13
|
+
begin
|
14
|
+
resp = ssm.get_parameter(
|
15
|
+
name: value,
|
16
|
+
with_decryption: true
|
17
|
+
)
|
18
|
+
rescue Aws::SSM::Errors::ParameterNotFound
|
19
|
+
raise ParameterNotFound, "Unable to find #{value} in Parameter Store"
|
20
|
+
end
|
21
|
+
resp.parameter.value
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def ssm
|
27
|
+
@ssm ||= Aws::SSM::Client.new(region: @stack_definition.region)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'os'
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
module ParameterResolvers
|
5
|
+
class Secret < Resolver
|
6
|
+
SecretNotFound = Class.new(StandardError)
|
7
|
+
PlatformNotSupported = Class.new(StandardError)
|
8
|
+
|
9
|
+
unless OS.windows?
|
10
|
+
require 'dotgpg'
|
11
|
+
array_resolver
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(config, stack_definition)
|
15
|
+
@config = config
|
16
|
+
@stack_definition = stack_definition
|
17
|
+
end
|
18
|
+
|
19
|
+
def resolve(value)
|
20
|
+
raise PlatformNotSupported, "The GPG Secret Parameter Resolver does not support Windows" if OS.windows?
|
21
|
+
secret_key = value
|
22
|
+
raise ArgumentError, "No secret_file defined for stack definition #{@stack_definition.stack_name} in #{@stack_definition.region}" unless !@stack_definition.secret_file.nil?
|
23
|
+
raise ArgumentError, "Could not find secret file at #{secret_file_path}" unless File.exist?(secret_file_path)
|
24
|
+
secrets_hash.fetch(secret_key) do
|
25
|
+
raise SecretNotFound, "Unable to find key #{secret_key} in file #{secret_file_path}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def secrets_hash
|
32
|
+
@secrets_hash ||= YAML.load(decrypt_with_dotgpg)
|
33
|
+
end
|
34
|
+
|
35
|
+
def decrypt_with_dotgpg
|
36
|
+
Dotgpg.interactive = true
|
37
|
+
dir = Dotgpg::Dir.closest(secret_file_path)
|
38
|
+
stream = StringIO.new
|
39
|
+
dir.decrypt(secret_path_relative_to_base, stream)
|
40
|
+
stream.string
|
41
|
+
end
|
42
|
+
|
43
|
+
def secret_path_relative_to_base
|
44
|
+
@secret_path_relative_to_base ||= File.join('secrets', @stack_definition.secret_file)
|
45
|
+
end
|
46
|
+
|
47
|
+
def secret_file_path
|
48
|
+
@secret_file_path ||= File.join(@config.base_dir, secret_path_relative_to_base)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class SecurityGroup < Resolver
|
4
|
+
array_resolver
|
5
|
+
|
6
|
+
def initialize(config, stack_definition)
|
7
|
+
@config = config
|
8
|
+
@stack_definition = stack_definition
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolve(value)
|
12
|
+
security_group_finder.find(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def security_group_finder
|
18
|
+
StackMaster::SecurityGroupFinder.new(@stack_definition.region)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class SnsTopicName < Resolver
|
4
|
+
TopicNotFound = Class.new(StandardError)
|
5
|
+
|
6
|
+
array_resolver
|
7
|
+
|
8
|
+
def initialize(config, stack_definition)
|
9
|
+
@config = config
|
10
|
+
@stack_definition = stack_definition
|
11
|
+
@stacks = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve(value)
|
15
|
+
sns_topic_finder.find(value)
|
16
|
+
rescue StackMaster::SnsTopicFinder::TopicNotFound => e
|
17
|
+
raise TopicNotFound.new(e.message)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def cf
|
23
|
+
@cf ||= StackMaster.cloud_formation_driver
|
24
|
+
end
|
25
|
+
|
26
|
+
def sns_topic_finder
|
27
|
+
StackMaster::SnsTopicFinder.new(@stack_definition.region)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class StackOutput < Resolver
|
4
|
+
StackNotFound = Class.new(StandardError)
|
5
|
+
StackOutputNotFound = Class.new(StandardError)
|
6
|
+
|
7
|
+
array_resolver
|
8
|
+
|
9
|
+
def initialize(config, stack_definition)
|
10
|
+
@config = config
|
11
|
+
@stack_definition = stack_definition
|
12
|
+
@stacks = {}
|
13
|
+
@cf_drivers = {}
|
14
|
+
@output_regex = %r{(?:(?<region>[^:]+):)?(?<stack_name>[^:/]+)/(?<output_name>.+)}
|
15
|
+
end
|
16
|
+
|
17
|
+
def resolve(value)
|
18
|
+
region, stack_name, output_name = parse!(value)
|
19
|
+
stack = find_stack(stack_name, region)
|
20
|
+
if stack
|
21
|
+
output = stack.outputs.find { |stack_output| stack_output.output_key == output_name.camelize }
|
22
|
+
if output
|
23
|
+
output.output_value
|
24
|
+
else
|
25
|
+
raise StackOutputNotFound, "Stack exists (#{stack_name}), but output does not: #{output_name}"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
raise StackNotFound, "Stack in StackOutput not found: #{value}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def cf
|
35
|
+
@cf ||= StackMaster.cloud_formation_driver
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse!(value)
|
39
|
+
if !value.is_a?(String) || !(match = @output_regex.match(value))
|
40
|
+
raise ArgumentError, 'Stack output values must be in the form of [region:]stack-name/output_name'
|
41
|
+
end
|
42
|
+
|
43
|
+
[
|
44
|
+
match[:region] || cf.region,
|
45
|
+
match[:stack_name],
|
46
|
+
match[:output_name]
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_stack(stack_name, region)
|
51
|
+
unaliased_region = @config.unalias_region(region)
|
52
|
+
stack_key = stack_key(stack_name, unaliased_region)
|
53
|
+
|
54
|
+
@stacks.fetch(stack_key) do
|
55
|
+
regional_cf = cf_for_region(unaliased_region)
|
56
|
+
cf_stack = regional_cf.describe_stacks(stack_name: stack_name).stacks.first
|
57
|
+
@stacks[stack_key] = cf_stack
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def stack_key(stack_name, region)
|
62
|
+
"#{region}:#{stack_name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def cf_for_region(region)
|
66
|
+
return cf if cf.region == region
|
67
|
+
|
68
|
+
@cf_drivers.fetch(region) do
|
69
|
+
cloud_formation_driver = cf.class.new
|
70
|
+
cloud_formation_driver.set_region(region)
|
71
|
+
@cf_drivers[region] = cloud_formation_driver
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module Prompter
|
3
|
+
def ask?(question)
|
4
|
+
StackMaster.stdout.print question
|
5
|
+
answer = if StackMaster.interactive?
|
6
|
+
if StackMaster.stdin.tty? && StackMaster.stdout.tty?
|
7
|
+
StackMaster.stdin.getch.chomp
|
8
|
+
else
|
9
|
+
StackMaster.stdout.puts
|
10
|
+
StackMaster.stdout.puts "STDOUT or STDIN was not a TTY. Defaulting to no. To force yes use -y"
|
11
|
+
'n'
|
12
|
+
end
|
13
|
+
else
|
14
|
+
print StackMaster.non_interactive_answer
|
15
|
+
StackMaster.non_interactive_answer
|
16
|
+
end
|
17
|
+
StackMaster.stdout.puts
|
18
|
+
answer == 'y'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module StackMaster
|
2
|
+
module ParameterResolvers
|
3
|
+
class ResolverArray
|
4
|
+
def initialize(config, stack_definition)
|
5
|
+
@config = config
|
6
|
+
@stack_definition = stack_definition
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolve(values)
|
10
|
+
Array(values).map do |value|
|
11
|
+
resolver_class.new(@config, @stack_definition).resolve(value)
|
12
|
+
end.join(',')
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolver_class
|
16
|
+
fail "Method resolver_class not implemented on #{self.class}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Resolver
|
21
|
+
def self.array_resolver(options = {})
|
22
|
+
resolver_class ||= Object.const_get(self.name)
|
23
|
+
array_resolver_class_name = options[:class_name] || resolver_class.to_s.demodulize.pluralize
|
24
|
+
|
25
|
+
klass = Class.new(ResolverArray) do
|
26
|
+
define_method('resolver_class') do
|
27
|
+
resolver_class
|
28
|
+
end
|
29
|
+
end
|
30
|
+
StackMaster::ParameterResolvers.const_set("#{array_resolver_class_name}", klass)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module StackMaster
|
2
|
+
class SecurityGroupFinder
|
3
|
+
SecurityGroupNotFound = Class.new(StandardError)
|
4
|
+
MultipleSecurityGroupsFound = Class.new(StandardError)
|
5
|
+
|
6
|
+
def initialize(region)
|
7
|
+
@resource = Aws::EC2::Resource.new(region: region)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find(reference)
|
11
|
+
raise ArgumentError, 'Security group references must be non-empty strings' unless reference.is_a?(String) && !reference.empty?
|
12
|
+
|
13
|
+
groups = @resource.security_groups({
|
14
|
+
filters: [
|
15
|
+
{
|
16
|
+
name: "group-name",
|
17
|
+
values: [reference],
|
18
|
+
},
|
19
|
+
],
|
20
|
+
})
|
21
|
+
|
22
|
+
raise SecurityGroupNotFound, "No security group with name #{reference} found" unless groups.any?
|
23
|
+
raise MultipleSecurityGroupsFound, "More than one security group with name #{reference} found" if groups.count > 1
|
24
|
+
|
25
|
+
groups.first.id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module StackMaster
|
2
|
+
class SnsTopicFinder
|
3
|
+
TopicNotFound = Class.new(StandardError)
|
4
|
+
|
5
|
+
def initialize(region)
|
6
|
+
@resource = Aws::SNS::Resource.new(region: region)
|
7
|
+
end
|
8
|
+
|
9
|
+
def find(reference)
|
10
|
+
raise ArgumentError, 'SNS topic references must be non-empty strings' unless reference.is_a?(String) && !reference.empty?
|
11
|
+
|
12
|
+
topic = @resource.topics.detect { |t| topic_name_from_arn(t.arn) == reference }
|
13
|
+
|
14
|
+
raise TopicNotFound, "No topic with name #{reference} found" unless topic
|
15
|
+
|
16
|
+
topic.arn
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def topic_name_from_arn(arn)
|
22
|
+
arn.split(":")[5]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'value_validator'
|
2
|
+
|
3
|
+
module StackMaster
|
4
|
+
module SparkleFormation
|
5
|
+
module CompileTime
|
6
|
+
class AllowedPatternValidator < ValueValidator
|
7
|
+
|
8
|
+
KEY = :allowed_pattern
|
9
|
+
|
10
|
+
def initialize(name, definition, parameter)
|
11
|
+
@name = name
|
12
|
+
@definition = definition
|
13
|
+
@parameter = parameter
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def check_is_valid
|
19
|
+
return true unless @definition.key?(KEY)
|
20
|
+
invalid_values.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def invalid_values
|
24
|
+
values = build_values(@definition, @parameter)
|
25
|
+
values.reject { |value| value.to_s.match(%r{#{@definition[KEY]}}) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_error
|
29
|
+
"#{@name}:#{invalid_values} does not match #{KEY}:#{@definition[KEY]}"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|