dd-vault 0.12.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.
- checksums.yaml +7 -0
- data/.gitignore +42 -0
- data/.rspec +2 -0
- data/.travis.yml +26 -0
- data/CHANGELOG.md +228 -0
- data/Gemfile +3 -0
- data/LICENSE +362 -0
- data/README.md +214 -0
- data/Rakefile +6 -0
- data/lib/vault/api/approle.rb +218 -0
- data/lib/vault/api/auth.rb +316 -0
- data/lib/vault/api/auth_tls.rb +92 -0
- data/lib/vault/api/auth_token.rb +242 -0
- data/lib/vault/api/help.rb +33 -0
- data/lib/vault/api/logical.rb +150 -0
- data/lib/vault/api/secret.rb +156 -0
- data/lib/vault/api/sys/audit.rb +91 -0
- data/lib/vault/api/sys/auth.rb +116 -0
- data/lib/vault/api/sys/health.rb +63 -0
- data/lib/vault/api/sys/init.rb +83 -0
- data/lib/vault/api/sys/leader.rb +48 -0
- data/lib/vault/api/sys/lease.rb +49 -0
- data/lib/vault/api/sys/mount.rb +103 -0
- data/lib/vault/api/sys/policy.rb +92 -0
- data/lib/vault/api/sys/seal.rb +81 -0
- data/lib/vault/api/sys.rb +25 -0
- data/lib/vault/api.rb +12 -0
- data/lib/vault/client.rb +447 -0
- data/lib/vault/configurable.rb +48 -0
- data/lib/vault/defaults.rb +197 -0
- data/lib/vault/encode.rb +19 -0
- data/lib/vault/errors.rb +72 -0
- data/lib/vault/persistent/connection.rb +42 -0
- data/lib/vault/persistent/pool.rb +48 -0
- data/lib/vault/persistent/timed_stack_multi.rb +70 -0
- data/lib/vault/persistent.rb +1158 -0
- data/lib/vault/request.rb +43 -0
- data/lib/vault/response.rb +89 -0
- data/lib/vault/vendor/connection_pool/timed_stack.rb +178 -0
- data/lib/vault/vendor/connection_pool/version.rb +5 -0
- data/lib/vault/vendor/connection_pool.rb +150 -0
- data/lib/vault/version.rb +3 -0
- data/lib/vault.rb +49 -0
- data/vault.gemspec +30 -0
- metadata +185 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Vault
|
2
|
+
class Request
|
3
|
+
attr_reader :client
|
4
|
+
|
5
|
+
def initialize(client)
|
6
|
+
@client = client
|
7
|
+
end
|
8
|
+
|
9
|
+
# @return [String]
|
10
|
+
def to_s
|
11
|
+
"#<#{self.class.name}>"
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
def inspect
|
16
|
+
"#<#{self.class.name}:0x#{"%x" % (self.object_id << 1)}>"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
include EncodePath
|
22
|
+
|
23
|
+
# Removes the given header fields from options and returns the result. This
|
24
|
+
# modifies the given options in place.
|
25
|
+
#
|
26
|
+
# @param [Hash] options
|
27
|
+
#
|
28
|
+
# @return [Hash]
|
29
|
+
def extract_headers!(options = {})
|
30
|
+
extract = {
|
31
|
+
wrap_ttl: Vault::Client::WRAP_TTL_HEADER,
|
32
|
+
}
|
33
|
+
|
34
|
+
{}.tap do |h|
|
35
|
+
extract.each do |k,v|
|
36
|
+
if options[k]
|
37
|
+
h[v] = options.delete(k)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Vault
|
2
|
+
class Response
|
3
|
+
# Defines a new field. This is designed to be used by the subclass as a
|
4
|
+
# mini-DSL.
|
5
|
+
#
|
6
|
+
# @example Default
|
7
|
+
# field :data
|
8
|
+
#
|
9
|
+
# @example With a mutator
|
10
|
+
# field :present, as: :present?
|
11
|
+
#
|
12
|
+
# @param n [Symbol] the name of the field
|
13
|
+
# @option opts [Symbol] :as alias for method name
|
14
|
+
#
|
15
|
+
# @!visibility private
|
16
|
+
def self.field(n, opts = {})
|
17
|
+
self.fields[n] = opts
|
18
|
+
|
19
|
+
if opts[:as].nil?
|
20
|
+
attr_reader n
|
21
|
+
else
|
22
|
+
define_method(opts[:as]) do
|
23
|
+
instance_variable_get(:"@#{n}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the list of fields defined on this subclass.
|
29
|
+
# @!visibility private
|
30
|
+
def self.fields
|
31
|
+
@fields ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Decodes the given object (usually a Hash) into an instance of this class.
|
35
|
+
#
|
36
|
+
# @param object [Hash<Symbol, Object>]
|
37
|
+
def self.decode(object)
|
38
|
+
self.new(object)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(opts = {})
|
42
|
+
# Initialize all fields as nil to start
|
43
|
+
self.class.fields.each do |k, _|
|
44
|
+
instance_variable_set(:"@#{k}", nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
# For each supplied option, set the instance variable if it was defined
|
48
|
+
# as a field.
|
49
|
+
opts.each do |k, v|
|
50
|
+
if self.class.fields.key?(k)
|
51
|
+
opts = self.class.fields[k]
|
52
|
+
|
53
|
+
if (m = opts[:load]) && !v.nil?
|
54
|
+
v = m.call(v)
|
55
|
+
end
|
56
|
+
|
57
|
+
if opts[:freeze]
|
58
|
+
v = v.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
instance_variable_set(:"@#{k}", v)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create a hash-bashed representation of this response.
|
67
|
+
#
|
68
|
+
# @return [Hash]
|
69
|
+
def to_h
|
70
|
+
self.class.fields.inject({}) do |h, (k, opts)|
|
71
|
+
if opts[:as].nil?
|
72
|
+
h[k] = self.public_send(k)
|
73
|
+
else
|
74
|
+
h[k] = self.public_send(opts[:as])
|
75
|
+
end
|
76
|
+
|
77
|
+
if !h[k].nil? && !h[k].is_a?(Array) && h[k].respond_to?(:to_h)
|
78
|
+
h[k] = h[k].to_h
|
79
|
+
end
|
80
|
+
|
81
|
+
h
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def ==(other)
|
86
|
+
self.to_h == other.to_h
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Vault; end
|
5
|
+
|
6
|
+
##
|
7
|
+
# Raised when you attempt to retrieve a connection from a pool that has been
|
8
|
+
# shut down.
|
9
|
+
|
10
|
+
class Vault::ConnectionPool::PoolShuttingDownError < RuntimeError; end
|
11
|
+
|
12
|
+
##
|
13
|
+
# The TimedStack manages a pool of homogeneous connections (or any resource
|
14
|
+
# you wish to manage). Connections are created lazily up to a given maximum
|
15
|
+
# number.
|
16
|
+
|
17
|
+
# Examples:
|
18
|
+
#
|
19
|
+
# ts = TimedStack.new(1) { MyConnection.new }
|
20
|
+
#
|
21
|
+
# # fetch a connection
|
22
|
+
# conn = ts.pop
|
23
|
+
#
|
24
|
+
# # return a connection
|
25
|
+
# ts.push conn
|
26
|
+
#
|
27
|
+
# conn = ts.pop
|
28
|
+
# ts.pop timeout: 5
|
29
|
+
# #=> raises Timeout::Error after 5 seconds
|
30
|
+
|
31
|
+
module Vault
|
32
|
+
class ConnectionPool::TimedStack
|
33
|
+
|
34
|
+
##
|
35
|
+
# Creates a new pool with +size+ connections that are created from the given
|
36
|
+
# +block+.
|
37
|
+
|
38
|
+
def initialize(size = 0, &block)
|
39
|
+
@create_block = block
|
40
|
+
@created = 0
|
41
|
+
@que = []
|
42
|
+
@max = size
|
43
|
+
@mutex = Mutex.new
|
44
|
+
@resource = ConditionVariable.new
|
45
|
+
@shutdown_block = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be
|
50
|
+
# used by subclasses that extend TimedStack.
|
51
|
+
|
52
|
+
def push(obj, options = {})
|
53
|
+
@mutex.synchronize do
|
54
|
+
if @shutdown_block
|
55
|
+
@shutdown_block.call(obj)
|
56
|
+
else
|
57
|
+
store_connection obj, options
|
58
|
+
end
|
59
|
+
|
60
|
+
@resource.broadcast
|
61
|
+
end
|
62
|
+
end
|
63
|
+
alias_method :<<, :push
|
64
|
+
|
65
|
+
##
|
66
|
+
# Retrieves a connection from the stack. If a connection is available it is
|
67
|
+
# immediately returned. If no connection is available within the given
|
68
|
+
# timeout a Timeout::Error is raised.
|
69
|
+
#
|
70
|
+
# +:timeout+ is the only checked entry in +options+ and is preferred over
|
71
|
+
# the +timeout+ argument (which will be removed in a future release). Other
|
72
|
+
# options may be used by subclasses that extend TimedStack.
|
73
|
+
|
74
|
+
def pop(timeout = 0.5, options = {})
|
75
|
+
options, timeout = timeout, 0.5 if Hash === timeout
|
76
|
+
timeout = options.fetch :timeout, timeout
|
77
|
+
|
78
|
+
deadline = Time.now + timeout
|
79
|
+
@mutex.synchronize do
|
80
|
+
loop do
|
81
|
+
raise ConnectionPool::PoolShuttingDownError if @shutdown_block
|
82
|
+
return fetch_connection(options) if connection_stored?(options)
|
83
|
+
|
84
|
+
connection = try_create(options)
|
85
|
+
return connection if connection
|
86
|
+
|
87
|
+
to_wait = deadline - Time.now
|
88
|
+
raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0
|
89
|
+
@resource.wait(@mutex, to_wait)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Shuts down the TimedStack which prevents connections from being checked
|
96
|
+
# out. The +block+ is called once for each connection on the stack.
|
97
|
+
|
98
|
+
def shutdown(&block)
|
99
|
+
raise ArgumentError, "shutdown must receive a block" unless block_given?
|
100
|
+
|
101
|
+
@mutex.synchronize do
|
102
|
+
@shutdown_block = block
|
103
|
+
@resource.broadcast
|
104
|
+
|
105
|
+
shutdown_connections
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Returns +true+ if there are no available connections.
|
111
|
+
|
112
|
+
def empty?
|
113
|
+
(@created - @que.length) >= @max
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# The number of connections available on the stack.
|
118
|
+
|
119
|
+
def length
|
120
|
+
@max - @created + @que.length
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
##
|
126
|
+
# This is an extension point for TimedStack and is called with a mutex.
|
127
|
+
#
|
128
|
+
# This method must returns true if a connection is available on the stack.
|
129
|
+
|
130
|
+
def connection_stored?(options = nil)
|
131
|
+
!@que.empty?
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# This is an extension point for TimedStack and is called with a mutex.
|
136
|
+
#
|
137
|
+
# This method must return a connection from the stack.
|
138
|
+
|
139
|
+
def fetch_connection(options = nil)
|
140
|
+
@que.pop
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# This is an extension point for TimedStack and is called with a mutex.
|
145
|
+
#
|
146
|
+
# This method must shut down all connections on the stack.
|
147
|
+
|
148
|
+
def shutdown_connections(options = nil)
|
149
|
+
while connection_stored?(options)
|
150
|
+
conn = fetch_connection(options)
|
151
|
+
@shutdown_block.call(conn)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# This is an extension point for TimedStack and is called with a mutex.
|
157
|
+
#
|
158
|
+
# This method must return +obj+ to the stack.
|
159
|
+
|
160
|
+
def store_connection(obj, options = nil)
|
161
|
+
@que.push obj
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# This is an extension point for TimedStack and is called with a mutex.
|
166
|
+
#
|
167
|
+
# This method must create a connection if and only if the total number of
|
168
|
+
# connections allowed has not been met.
|
169
|
+
|
170
|
+
def try_create(options = nil)
|
171
|
+
unless @created == @max
|
172
|
+
object = @create_block.call
|
173
|
+
@created += 1
|
174
|
+
object
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require_relative 'connection_pool/version'
|
2
|
+
require_relative 'connection_pool/timed_stack'
|
3
|
+
|
4
|
+
|
5
|
+
# Generic connection pool class for e.g. sharing a limited number of network connections
|
6
|
+
# among many threads. Note: Connections are lazily created.
|
7
|
+
#
|
8
|
+
# Example usage with block (faster):
|
9
|
+
#
|
10
|
+
# @pool = ConnectionPool.new { Redis.new }
|
11
|
+
#
|
12
|
+
# @pool.with do |redis|
|
13
|
+
# redis.lpop('my-list') if redis.llen('my-list') > 0
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Using optional timeout override (for that single invocation)
|
17
|
+
#
|
18
|
+
# @pool.with(:timeout => 2.0) do |redis|
|
19
|
+
# redis.lpop('my-list') if redis.llen('my-list') > 0
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Example usage replacing an existing connection (slower):
|
23
|
+
#
|
24
|
+
# $redis = ConnectionPool.wrap { Redis.new }
|
25
|
+
#
|
26
|
+
# def do_work
|
27
|
+
# $redis.lpop('my-list') if $redis.llen('my-list') > 0
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Accepts the following options:
|
31
|
+
# - :size - number of connections to pool, defaults to 5
|
32
|
+
# - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
|
33
|
+
#
|
34
|
+
module Vault
|
35
|
+
class ConnectionPool
|
36
|
+
DEFAULTS = {size: 5, timeout: 5}
|
37
|
+
|
38
|
+
class Error < RuntimeError
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.wrap(options, &block)
|
42
|
+
Wrapper.new(options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(options = {}, &block)
|
46
|
+
raise ArgumentError, 'Connection pool requires a block' unless block
|
47
|
+
|
48
|
+
options = DEFAULTS.merge(options)
|
49
|
+
|
50
|
+
@size = options.fetch(:size)
|
51
|
+
@timeout = options.fetch(:timeout)
|
52
|
+
|
53
|
+
@available = TimedStack.new(@size, &block)
|
54
|
+
@key = :"current-#{@available.object_id}"
|
55
|
+
end
|
56
|
+
|
57
|
+
if Thread.respond_to?(:handle_interrupt)
|
58
|
+
|
59
|
+
# MRI
|
60
|
+
def with(options = {})
|
61
|
+
Thread.handle_interrupt(Exception => :never) do
|
62
|
+
conn = checkout(options)
|
63
|
+
begin
|
64
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
65
|
+
yield conn
|
66
|
+
end
|
67
|
+
ensure
|
68
|
+
checkin
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
else
|
74
|
+
|
75
|
+
# jruby 1.7.x
|
76
|
+
def with(options = {})
|
77
|
+
conn = checkout(options)
|
78
|
+
begin
|
79
|
+
yield conn
|
80
|
+
ensure
|
81
|
+
checkin
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
def checkout(options = {})
|
88
|
+
conn = if stack.empty?
|
89
|
+
timeout = options[:timeout] || @timeout
|
90
|
+
@available.pop(timeout: timeout)
|
91
|
+
else
|
92
|
+
stack.last
|
93
|
+
end
|
94
|
+
|
95
|
+
stack.push conn
|
96
|
+
conn
|
97
|
+
end
|
98
|
+
|
99
|
+
def checkin
|
100
|
+
conn = pop_connection # mutates stack, must be on its own line
|
101
|
+
@available.push(conn) if stack.empty?
|
102
|
+
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def shutdown(&block)
|
107
|
+
@available.shutdown(&block)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def pop_connection
|
113
|
+
if stack.empty?
|
114
|
+
raise ConnectionPool::Error, 'no connections are checked out'
|
115
|
+
else
|
116
|
+
stack.pop
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def stack
|
121
|
+
::Thread.current[@key] ||= []
|
122
|
+
end
|
123
|
+
|
124
|
+
class Wrapper < ::BasicObject
|
125
|
+
METHODS = [:with, :pool_shutdown]
|
126
|
+
|
127
|
+
def initialize(options = {}, &block)
|
128
|
+
@pool = ::ConnectionPool.new(options, &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
def with(&block)
|
132
|
+
@pool.with(&block)
|
133
|
+
end
|
134
|
+
|
135
|
+
def pool_shutdown(&block)
|
136
|
+
@pool.shutdown(&block)
|
137
|
+
end
|
138
|
+
|
139
|
+
def respond_to?(id, *args)
|
140
|
+
METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def method_missing(name, *args, &block)
|
144
|
+
with do |connection|
|
145
|
+
connection.send(name, *args, &block)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/vault.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Vault
|
2
|
+
require_relative "vault/errors"
|
3
|
+
require_relative "vault/client"
|
4
|
+
require_relative "vault/configurable"
|
5
|
+
require_relative "vault/defaults"
|
6
|
+
require_relative "vault/response"
|
7
|
+
require_relative "vault/version"
|
8
|
+
|
9
|
+
require_relative "vault/api"
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# API client object based off the configured options in {Configurable}.
|
13
|
+
#
|
14
|
+
# @return [Vault::Client]
|
15
|
+
attr_reader :client
|
16
|
+
|
17
|
+
def setup!
|
18
|
+
@client = Vault::Client.new
|
19
|
+
|
20
|
+
# Set secure SSL options
|
21
|
+
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options].tap do |opts|
|
22
|
+
opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS)
|
23
|
+
opts |= OpenSSL::SSL::OP_NO_COMPRESSION if defined?(OpenSSL::SSL::OP_NO_COMPRESSION)
|
24
|
+
opts |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
|
25
|
+
opts |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
|
26
|
+
end
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Delegate all methods to the client object, essentially making the module
|
32
|
+
# object behave like a {Client}.
|
33
|
+
def method_missing(m, *args, &block)
|
34
|
+
if @client.respond_to?(m)
|
35
|
+
@client.send(m, *args, &block)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Delegating +respond_to+ to the {Client}.
|
42
|
+
def respond_to_missing?(m, include_private = false)
|
43
|
+
@client.respond_to?(m, include_private) || super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Load the initial default values
|
49
|
+
Vault.setup!
|
data/vault.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "vault/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dd-vault"
|
8
|
+
spec.version = Vault::VERSION
|
9
|
+
spec.authors = ["Seth Vargo"]
|
10
|
+
spec.email = ["sethvargo@gmail.com"]
|
11
|
+
spec.licenses = ["MPL-2.0"]
|
12
|
+
|
13
|
+
spec.summary = "Vault is a Ruby API client for interacting with a Vault server."
|
14
|
+
spec.description = spec.summary
|
15
|
+
spec.homepage = "https://github.com/hashicorp/vault-ruby"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "aws-sigv4"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
28
|
+
spec.add_development_dependency "yard"
|
29
|
+
spec.add_development_dependency "webmock", "~> 2.3"
|
30
|
+
end
|