consul-templaterb 1.2.1 → 1.3.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 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