consul-templaterb 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ecbf300312175e355f65232450dc9303db8bbb1d2d2af473480a2fde3997112
4
- data.tar.gz: 08abe3b2ef2e812810e88d29db8a44c1191af395a3e5b94c91a9d2a54b0c5906
3
+ metadata.gz: 875f33bec50159a51a9fb9e59b07fbe37a78a0587cb3a496cc42a1806b5c8c9d
4
+ data.tar.gz: 08f091608892658b875d716c6935c9fe24f19b72b0c37dcde07eee4bcbdea838
5
5
  SHA512:
6
- metadata.gz: 8042decc8b615eff3080f06330347f6596f7fbdb5b8e732eaf9afc00b79a7b2ddffb161d3c9c63d986f7eed2d54a467cca0d39d9ee247af65edad71453fcbaf5
7
- data.tar.gz: b88e62b6884d6dfc3e0dad669943124e6ec0c28af3da482c6a9614f2b23325bd9d57fe9afeb0ebd08c57808a62003ab3a4dd4cb4da1b008043ec865bf49e054f
6
+ metadata.gz: 481afae4f8b4df204897584ebc0ad4559c54ea5cbdd4a693977c33a573acc67e3e726df8b55a6ed3d2506b01c82f3092d98d27208e0de51c1c2367df01447cb3
7
+ data.tar.gz: 361f0786469ff9c305f047824b42a8c801109ffe6c7283e0a4325c2f89c3d8dcfa989b3a52bbef982e8cc78bbad17505b2c5c2b57223b67ba0343c0c9722143a
data/CHANGELOG.md CHANGED
@@ -2,8 +2,20 @@
2
2
 
3
3
  ## (UNRELEASED)
4
4
 
5
+ ## 1.3.0 (June 7, 2018)
6
+
5
7
  IMPROVEMENTS:
6
8
 
9
+ * samples/consul-ui/ now supports keys as well as nodes thans to [@geobeau](https://github.com/geobeau)
10
+
11
+ NEW FEATURES
12
+
13
+ * EXPERIMENTAL Vault support thanks to [@uepock](https://github.com/uepoch)
14
+
15
+ BUG FIXES:
16
+
17
+ * Properly shutdown connections when receiving Posix signal to kill app
18
+
7
19
  ## 1.2.1 (May 28, 2018)
8
20
 
9
21
  BUG FIXES:
data/README.md CHANGED
@@ -276,7 +276,7 @@ Please consult [CHANGELOG.md](CHANGELOG.md) for fixed bugs.
276
276
 
277
277
  ## TODO
278
278
 
279
- * [ ] Hashi's Vault support
279
+ * [x] Hashi's Vault support (EXPERIMENTAL)
280
280
  * [ ] Implement automatic dynamic rate limit
281
281
  * [ ] More samples: apache, nginx, full website displaying consul information...
282
282
  * [ ] Optimize rendering speed at start-up: an iteration is done very second by default, but it would be possible to speed
@@ -17,6 +17,24 @@ def compute_default_output(source)
17
17
  end
18
18
 
19
19
  options = {
20
+ vault: {
21
+ debug: {
22
+ network: false
23
+ },
24
+ base_url: ENV['VAULT_ADDR'] || 'http://localhost:8200',
25
+ token: ENV['VAULT_TOKEN'] || nil,
26
+ token_renew: true,
27
+ retry_duration: 10,
28
+ lease_duration_factor: 0.5, # The time it waits before actualizing data based on the lease time: 2h lease * 0.5 = Fetch data every 1h
29
+ min_duration: 60,
30
+ max_retry_duration: 86_400, # Much higher than consul's because some dynamic secrets could be used for a day.
31
+ paths: {
32
+ '/v1/sys/mounts' => {
33
+ max_retry_duration: 600,
34
+ min_duration: 600
35
+ }
36
+ }
37
+ },
20
38
  consul: {
21
39
  debug: {
22
40
  network: false
@@ -71,10 +89,27 @@ optparse = OptionParser.new do |opts|
71
89
  options[:consul][:base_url] = consul_url
72
90
  end
73
91
 
74
- opts.on('-t', '--consul-token=<token>', String, 'Use a token to connect to Consul') do |consul_token|
92
+ opts.on('--consul-token=<token>', String, 'Use a token to connect to Consul') do |consul_token|
75
93
  options[:consul][:token] = consul_token
76
94
  end
77
95
 
96
+ opts.on('-V', '--vault-addr=<address>', String, 'Address of Vault, eg: http://localhost:8200') do |vault_url|
97
+ options[:vault][:base_url] = vault_url
98
+ end
99
+
100
+ opts.on('-T', '--vault-token=<token>', String, 'Token used to authenticate against vault.') do |vault_token|
101
+ options[:vault][:token] = vault_token
102
+ end
103
+
104
+ options[:vault][:token_renew] = true
105
+ opts.on('--[no-]vault-renew', 'Control auto-renewal of the Vault token. Default: activated') do |vault_renew|
106
+ options[:vault][:token_renew] = vault_renew
107
+ end
108
+
109
+ opts.on('--vault-lease-duration-factor=<factor>', Float, 'Wait at least <factor> * lease time before updating a Vault secret. Default: 0.5') do |factor|
110
+ options[:vault][:lease_duration_factor] = factor
111
+ end
112
+
78
113
  opts.on('-w', '--wait=<min_duration>', Float, 'Wait at least n seconds before each template generation') do |min_duration|
79
114
  options[:consul][:min_duration] = min_duration
80
115
  end
@@ -213,8 +248,10 @@ def find_max_descriptors(max_descripors)
213
248
  max_size
214
249
  end
215
250
 
216
- unless options[:consul][:base_url].start_with? 'http'
217
- options[:consul][:base_url] = "http://#{options[:consul][:base_url]}"
251
+ %i[consul vault].each do |type|
252
+ unless options[type][:base_url].start_with? 'http', 'https'
253
+ options[type][:base_url] = "http://#{options[type][:base_url]}"
254
+ end
218
255
  end
219
256
 
220
257
  # Since we might be using a lots of descriptors, document this
@@ -227,7 +264,8 @@ STDERR.puts "Max number of descriptors set to #{new_size}" if options[:consul][:
227
264
  EM.epoll
228
265
 
229
266
  consul_conf = Consul::Async::ConsulConfiguration.new(options[:consul])
230
- template_manager = Consul::Async::ConsulEndPointsManager.new(consul_conf)
267
+ vault_conf = Consul::Async::VaultConfiguration.new(options[:vault])
268
+ template_manager = Consul::Async::EndPointsManager.new(consul_conf, vault_conf)
231
269
 
232
270
  ARGV.each do |tpl|
233
271
  dest = compute_default_output(tpl)
@@ -239,6 +277,7 @@ end
239
277
  %w[INT PIPE TERM].each do |sig|
240
278
  Signal.trap(sig) do
241
279
  STDERR.puts "[KILL] received #{sig}, stopping myself"
280
+ template_manager.terminate
242
281
  kill_program
243
282
  end
244
283
  end
@@ -1,4 +1,5 @@
1
1
  require 'consul/async/utilities'
2
+ require 'consul/async/stats'
2
3
  require 'em-http'
3
4
  require 'thread'
4
5
  require 'json'
@@ -56,38 +57,6 @@ module Consul
56
57
  paths: @paths)
57
58
  end
58
59
  end
59
- class ConsulEndPointStats
60
- attr_reader :successes, :errors, :start, :body_bytes
61
- def initialize
62
- @start = Time.now.utc
63
- @successes = 0
64
- @errors = 0
65
- @body_bytes = 0
66
- end
67
-
68
- def on_reponse(res)
69
- @successes += 1
70
- @body_bytes = body_bytes + res.http.response.bytesize
71
- end
72
-
73
- def on_error(_http)
74
- @errors += 1
75
- end
76
-
77
- def bytes_per_sec
78
- diff = (Time.now.utc - start)
79
- diff = 1 if diff < 1
80
- (body_bytes / diff).round(0)
81
- end
82
-
83
- def bytes_per_sec_human
84
- "#{Utilities.bytes_to_h(bytes_per_sec)}/s"
85
- end
86
-
87
- def body_bytes_human
88
- Utilities.bytes_to_h(body_bytes)
89
- end
90
- end
91
60
  class ConsulResult
92
61
  attr_reader :data, :http, :x_consul_index, :last_update, :stats, :retry_in
93
62
  def initialize(data, modified, http, x_consul_index, stats, retry_in)
@@ -106,6 +75,7 @@ module Consul
106
75
 
107
76
  def mutate(new_data)
108
77
  @data = new_data.dup
78
+ @data_json = nil
109
79
  end
110
80
 
111
81
  def json
@@ -146,9 +116,9 @@ module Consul
146
116
  @consecutive_errors = 0
147
117
  @query_params = query_params
148
118
  @stopping = false
149
- @stats = ConsulEndPointStats.new
119
+ @stats = EndPointStats.new
150
120
  @last_result = ConsulResult.new(default_value, false, HttpResponse.new(nil), 0, stats, 1)
151
- on_response { |result| @stats.on_reponse result }
121
+ on_response { |result| @stats.on_response result }
152
122
  on_error { |http| @stats.on_error http }
153
123
  _enable_network_debug if conf.debug && conf.debug[:network]
154
124
  fetch
@@ -19,10 +19,11 @@ module Consul
19
19
  end
20
20
  end
21
21
 
22
- class ConsulEndPointsManager
23
- attr_reader :conf, :net_info, :start_time
24
- def initialize(consul_configuration)
25
- @conf = consul_configuration
22
+ class EndPointsManager
23
+ attr_reader :consul_conf, :vault_conf, :net_info, :start_time
24
+ def initialize(consul_configuration, vault_configuration)
25
+ @consul_conf = consul_configuration
26
+ @vault_conf = vault_configuration
26
27
  @endpoints = {}
27
28
  @iteration = 1
28
29
  @start_time = Time.now.utc
@@ -35,6 +36,11 @@ module Consul
35
36
  current_erb_path: nil,
36
37
  params: {}
37
38
  }
39
+
40
+ unless @vault_conf.token.nil? || !@vault_conf.token_renew
41
+ #Setup token renewal
42
+ vault_setup_token_renew
43
+ end
38
44
  end
39
45
 
40
46
  # https://www.consul.io/api/health.html#list-nodes-for-service
@@ -45,7 +51,7 @@ module Consul
45
51
  query_params[:dc] = dc if dc
46
52
  query_params[:passing] = passing if passing
47
53
  query_params[:tag] = tag if tag
48
- create_if_missing(path, query_params) { ConsulTemplateService.new(ConsulEndpoint.new(conf, path, true, query_params, '[]')) }
54
+ create_if_missing(path, query_params) { ConsulTemplateService.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]')) }
49
55
  end
50
56
 
51
57
  # https://www.consul.io/api/health.html#list-checks-for-service
@@ -55,7 +61,7 @@ module Consul
55
61
  query_params = {}
56
62
  query_params[:dc] = dc if dc
57
63
  query_params[:passing] = passing if passing
58
- create_if_missing(path, query_params) { ConsulTemplateChecks.new(ConsulEndpoint.new(conf, path, true, query_params, '[]')) }
64
+ create_if_missing(path, query_params) { ConsulTemplateChecks.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]')) }
59
65
  end
60
66
 
61
67
  # https://www.consul.io/api/catalog.html#list-nodes
@@ -63,7 +69,7 @@ module Consul
63
69
  path = '/v1/catalog/nodes'
64
70
  query_params = {}
65
71
  query_params[:dc] = dc if dc
66
- create_if_missing(path, query_params) { ConsulTemplateNodes.new(ConsulEndpoint.new(conf, path, true, query_params, '[]')) }
72
+ create_if_missing(path, query_params) { ConsulTemplateNodes.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]')) }
67
73
  end
68
74
 
69
75
  # https://www.consul.io/api/catalog.html#list-services-for-node
@@ -71,7 +77,7 @@ module Consul
71
77
  path = "/v1/catalog/node/#{name_or_id}"
72
78
  query_params = {}
73
79
  query_params[:dc] = dc if dc
74
- create_if_missing(path, query_params) { ConsulTemplateNodes.new(ConsulEndpoint.new(conf, path, true, query_params, '{}')) }
80
+ create_if_missing(path, query_params) { ConsulTemplateNodes.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '{}')) }
75
81
  end
76
82
 
77
83
  # https://www.consul.io/api/agent.html#read-configuration
@@ -79,7 +85,7 @@ module Consul
79
85
  path = '/v1/agent/self'
80
86
  query_params = {}
81
87
  default_value = '{"Config":{}, "Coord":{}, "Member":{}, "Meta":{}, "Stats":{}}'
82
- create_if_missing(path, query_params) { ConsulAgentSelf.new(ConsulEndpoint.new(conf, path, true, query_params, default_value)) }
88
+ create_if_missing(path, query_params) { ConsulAgentSelf.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value)) }
83
89
  end
84
90
 
85
91
  # https://www.consul.io/api/agent.html#view-metrics
@@ -87,7 +93,7 @@ module Consul
87
93
  path = '/v1/agent/metrics'
88
94
  query_params = {}
89
95
  default_value = '{"Gauges":[], "Points":[], "Member":{}, "Counters":[], "Samples":{}}'
90
- create_if_missing(path, query_params) { ConsulAgentMetrics.new(ConsulEndpoint.new(conf, path, true, query_params, default_value)) }
96
+ create_if_missing(path, query_params) { ConsulAgentMetrics.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value)) }
91
97
  end
92
98
 
93
99
  # Return a param of template
@@ -105,14 +111,14 @@ module Consul
105
111
  query_params[:dc] = dc if dc
106
112
  # Tag filtering is performed on client side
107
113
  query_params[:tag] = tag if tag
108
- create_if_missing(path, query_params) { ConsulTemplateServices.new(ConsulEndpoint.new(conf, path, true, query_params, '{}')) }
114
+ create_if_missing(path, query_params) { ConsulTemplateServices.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '{}')) }
109
115
  end
110
116
 
111
117
  # https://www.consul.io/api/catalog.html#list-datacenters
112
118
  def datacenters
113
119
  path = '/v1/catalog/datacenters'
114
120
  query_params = {}
115
- create_if_missing(path, query_params) { ConsulTemplateDatacenters.new(ConsulEndpoint.new(conf, path, true, query_params, '[]')) }
121
+ create_if_missing(path, query_params) { ConsulTemplateDatacenters.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]')) }
116
122
  end
117
123
 
118
124
  # https://www.consul.io/api/kv.html#read-key
@@ -123,7 +129,23 @@ module Consul
123
129
  query_params[:recurse] = recurse if recurse
124
130
  query_params[:keys] = keys if keys
125
131
  default_value = '[]'
126
- create_if_missing(path, query_params) { ConsulTemplateKV.new(ConsulEndpoint.new(conf, path, true, query_params, default_value), name) }
132
+ create_if_missing(path, query_params) { ConsulTemplateKV.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value), name) }
133
+ end
134
+
135
+ def secrets(path = '')
136
+ raise "You need to provide a vault token to use 'secret' keyword" if vault_conf.token.nil?
137
+ path = "/v1/#{path}".gsub(/\/{2,}/, '/')
138
+ query_params = {list: "true"}
139
+ create_if_missing(path, query_params) { ConsulTemplateVaultSecretList.new(VaultEndpoint.new(vault_conf, path, 'GET',true, query_params,JSON.generate(data: {keys: []}))) }
140
+ end
141
+
142
+ def secret(path = '', post_data = nil )
143
+ puts post_data
144
+ raise "You need to provide a vault token to use 'secrets' keyword" if vault_conf.token.nil?
145
+ path = "/v1/#{path}"
146
+ query_params = {}
147
+ method = post_data ? "POST" : "GET"
148
+ create_if_missing(path, query_params) { ConsulTemplateVaultSecret.new(VaultEndpoint.new(vault_conf, path, method, true, query_params, JSON.generate(data: {}))) }
127
149
  end
128
150
 
129
151
  # render a relative file with the given params accessible from template
@@ -204,6 +226,12 @@ module Consul
204
226
  @endpoints = {}
205
227
  end
206
228
 
229
+ def vault_setup_token_renew
230
+ path = 'v1/auth/token/renew-self'
231
+ STDERR.print "[INFO] Setting up vault token renewal"
232
+ VaultEndpoint.new(vault_conf, path, :POST, {}, {})
233
+ end
234
+
207
235
  def create_if_missing(path, query_params)
208
236
  fqdn = path.dup
209
237
  query_params.each_pair do |k, v|
@@ -384,5 +412,18 @@ module Consul
384
412
  end
385
413
  end
386
414
  end
415
+
416
+ class ConsulTemplateVaultSecret < ConsulTemplateAbstractMap
417
+ def initialize(vault_endpoint)
418
+ super(vault_endpoint)
419
+ end
420
+ end
421
+ class ConsulTemplateVaultSecretList < ConsulTemplateAbstractArray
422
+ def parse_result(res)
423
+ return res if res.data.nil?
424
+ res.mutate(JSON.generate(res.json['data']['keys']))
425
+ res
426
+ end
427
+ end
387
428
  end
388
429
  end
@@ -1,5 +1,6 @@
1
1
  require 'consul/async/utilities'
2
2
  require 'consul/async/consul_endpoint'
3
+ require 'consul/async/vault_endpoint'
3
4
  require 'consul/async/consul_template'
4
5
  require 'consul/async/consul_template_render'
5
6
  require 'em-http'
@@ -55,7 +56,7 @@ module Consul
55
56
  template_manager.terminate
56
57
  EventMachine.stop
57
58
  rescue StandardError => e
58
- STDERR.puts "[ERROR] Fatal error occured: #{e.inspect} - #{e.backtrace}"
59
+ STDERR.puts "[ERROR] Fatal error occured: #{e.inspect} - #{e.backtrace.join("\n\t")}"
59
60
  template_manager.terminate
60
61
  EventMachine.stop
61
62
  end
@@ -0,0 +1,137 @@
1
+ require 'consul/async/utilities'
2
+
3
+ class Endpoint
4
+ attr_reader :conf, :path, :x_consul_index, :queue, :stats, :last_result, :enforce_json_200, :start_time, :default_value, :query_params
5
+ def initialize(conf, path, enforce_json_200 = true, query_params = {}, default_value = '[]')
6
+ @conf = conf.create(path)
7
+ @default_value = default_value
8
+ @path = path
9
+ @queue = EM::Queue.new
10
+ @s_callbacks = []
11
+ @e_callbacks = []
12
+ @enforce_json_200 = enforce_json_200
13
+ @start_time = Time.now.utc
14
+ @consecutive_errors = 0
15
+ @query_params = query_params
16
+ @stopping = false
17
+ @stats = EndPointStats.new
18
+ @last_result = ConsulResult.new(default_value, false, HttpResponse.new(nil), 0, stats, 1)
19
+ on_response { |result| @stats.on_reponse result }
20
+ on_error { |http| @stats.on_error http }
21
+ _enable_network_debug if conf.debug && conf.debug[:network]
22
+ fetch
23
+ queue << 0
24
+ end
25
+
26
+ def _enable_network_debug
27
+ on_response do |result|
28
+ state = result.x_consul_index.to_i < 1 ? '[WARN]' : '[ OK ]'
29
+ stats = result.stats
30
+ STDERR.puts "[DEBUG]#{state}#{result.modified? ? '[MODIFIED]' : '[NO DIFF]'}" \
31
+ "[s:#{stats.successes},err:#{stats.errors}]" \
32
+ "[#{stats.body_bytes_human.ljust(8)}][#{stats.bytes_per_sec_human.ljust(9)}]"\
33
+ " #{path.ljust(48)} idx:#{result.x_consul_index}, next in #{result.retry_in} s"
34
+ end
35
+ on_error { |http| STDERR.puts "[ERROR]: #{path}: #{http.error}" }
36
+ end
37
+
38
+ def on_response(&block)
39
+ @s_callbacks << block
40
+ end
41
+
42
+ def on_error(&block)
43
+ @e_callbacks << block
44
+ end
45
+
46
+ def ready?
47
+ @ready
48
+ end
49
+
50
+ def terminate
51
+ @stopping = true
52
+ end
53
+
54
+ private
55
+
56
+ def build_request(headers = {}, query_params = {})
57
+ req = {
58
+ head: headers,
59
+ path: path,
60
+ query: query_params,
61
+ keepalive: true,
62
+ callback: method(:on_response)
63
+ }
64
+ @query_params.each_pair do |k, v|
65
+ req[:query][k] = v
66
+ end
67
+ req
68
+ end
69
+
70
+ def log(level, message)
71
+
72
+ end
73
+
74
+ def _handle_error(http, consul_index)
75
+ retry_in = [600, conf.retry_duration + 2**@consecutive_errors].min
76
+ STDERR.puts "[ERROR][#{path}] X-Consul-Index:#{consul_index} - #{http.error} - Retry in #{retry_in}s #{stats.body_bytes_human}"
77
+ @consecutive_errors += 1
78
+ http_result = HttpResponse.new(http)
79
+ EventMachine.add_timer(retry_in) do
80
+ yield
81
+ queue.push(consul_index)
82
+ end
83
+ @e_callbacks.each { |c| c.call(http_result) }
84
+ end
85
+
86
+ def fetch
87
+ options = {
88
+ connect_timeout: 5, # default connection setup timeout
89
+ inactivity_timeout: conf.wait_duration + 1, # default connection inactivity (post-setup) timeout
90
+ }
91
+ connection = EventMachine::HttpRequest.new(conf.base_url, options)
92
+ cb = proc do |consul_index|
93
+ http = connection.get(build_request(consul_index))
94
+ http.callback do
95
+ # Dirty hack, but contrary to other path, when key is not present, Consul returns 404
96
+ is_kv_empty = path.start_with?('/v1/kv') && http.response_header.status == 404
97
+ if !is_kv_empty && enforce_json_200 && http.response_header.status != 200 && http.response_header['Content-Type'] != 'application/json'
98
+ _handle_error(http, consul_index) { connection = EventMachine::HttpRequest.new(conf.base_url, options) }
99
+ else
100
+ n_consul_index = find_x_consul_token(http)
101
+ @consecutive_errors = 0
102
+ http_result = if is_kv_empty
103
+ HttpResponse.new(http, default_value)
104
+ else
105
+ HttpResponse.new(http)
106
+ end
107
+ new_content = http_result.response.freeze
108
+ modified = @last_result.nil? ? true : @last_result.data != new_content
109
+ if n_consul_index == consul_index || n_consul_index.nil?
110
+ retry_in = modified ? conf.missing_index_retry_time_on_diff : conf.missing_index_retry_time_on_unchanged
111
+ n_consul_index = consul_index
112
+ else
113
+ retry_in = modified ? conf.min_duration : conf.retry_on_non_diff
114
+ end
115
+ retry_in = 0.1 if retry_in < 0.1
116
+ unless @stopping
117
+ EventMachine.add_timer(retry_in) do
118
+ queue.push(n_consul_index)
119
+ end
120
+ end
121
+ result = ConsulResult.new(new_content, modified, http_result, n_consul_index, stats, retry_in)
122
+ @last_result = result
123
+ @ready = true
124
+ @s_callbacks.each { |c| c.call(result) }
125
+ end
126
+ end
127
+
128
+ http.errback do
129
+ unless @stopping
130
+ _handle_error(http, consul_index) { connection = EventMachine::HttpRequest.new(conf.base_url, options) }
131
+ end
132
+ end
133
+ queue.pop(&cb)
134
+ end
135
+ queue.pop(&cb)
136
+ end
137
+ end