Class: Consul::Async::EndPointsManager

Inherits:
Object
  • Object
show all
Defined in:
lib/consul/async/consul_template.rb

Overview

This class keep references over all endpoints (aka datasources) registered for all templates. This allows reusing those endpoints as well as performing listing and garbage collecting. This is also the main object visible from ERB files which contains all methods available to template writters.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(consul_configuration, vault_configuration, templates, trim_mode = nil) ⇒ EndPointsManager

Returns a new instance of EndPointsManager.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/consul/async/consul_template.rb', line 100

def initialize(consul_configuration, vault_configuration, templates, trim_mode = nil)
  @running = true
  @consul_conf = consul_configuration
  @vault_conf = vault_configuration
  @trim_mode = trim_mode
  @endpoints = {}
  @iteration = 1
  @start_time = Time.now.utc
  @last_debug_time = 0
  @net_info = {
    success: 0,
    errors: 0,
    bytes_read: 0,
    changes: 0,
    network_bytes: 0
  }
  @templates = templates
  @context = {
    current_erb_path: nil,
    template_info: {
      'source_root' => nil,
      'source' => nil,
      'destination' => nil,
      'was_rendered_once' => false
    },
    params: {}
  }
  @max_consecutive_errors_on_endpoint = consul_configuration.max_consecutive_errors_on_endpoint || 10
  @fail_fast_errors = consul_configuration.fail_fast_errors
  @coordinate = Coordinate.new(self)
  @remote_resource = RemoteResource.new(self)

  # Setup token renewal
  vault_setup_token_renew unless @vault_conf.token.nil? || !@vault_conf.token_renew
end

Instance Attribute Details

#consul_confObject (readonly)

Returns the value of attribute consul_conf.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def consul_conf
  @consul_conf
end

#coordinateObject (readonly)

Returns the value of attribute coordinate.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def coordinate
  @coordinate
end

#net_infoObject (readonly)

Returns the value of attribute net_info.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def net_info
  @net_info
end

#remote_resourceObject (readonly)

Returns the value of attribute remote_resource.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def remote_resource
  @remote_resource
end

#runningObject (readonly)

Returns the value of attribute running.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def running
  @running
end

#start_timeObject (readonly)

Returns the value of attribute start_time.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def start_time
  @start_time
end

#templatesObject (readonly)

Returns the value of attribute templates.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def templates
  @templates
end

#vault_confObject (readonly)

Returns the value of attribute vault_conf.



98
99
100
# File 'lib/consul/async/consul_template.rb', line 98

def vault_conf
  @vault_conf
end

Instance Method Details

#agent_members(wan: false, agent: nil) ⇒ Object



215
216
217
218
219
220
221
# File 'lib/consul/async/consul_template.rb', line 215

def agent_members(wan: false, agent: nil)
  path = '/v1/agent/members'
  query_params = {}
  query_params['wan'] = true if wan
  default_value = '[]'
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateMembers.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value, agent)) }
end

#agent_metrics(agent: nil) ⇒ Object



207
208
209
210
211
212
# File 'lib/consul/async/consul_template.rb', line 207

def agent_metrics(agent: nil)
  path = '/v1/agent/metrics'
  query_params = {}
  default_value = '{"Gauges":[], "Points":[], "Member":{}, "Counters":[], "Samples":{}}'
  create_if_missing(path, query_params, agent: agent) { ConsulAgentMetrics.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value, agent)) }
end

#agent_self(agent: nil) ⇒ Object



199
200
201
202
203
204
# File 'lib/consul/async/consul_template.rb', line 199

def agent_self(agent: nil)
  path = '/v1/agent/self'
  query_params = {}
  default_value = '{"Config":{}, "Coord":{}, "Member":{}, "Meta":{}, "Stats":{}}'
  create_if_missing(path, query_params, agent: agent) { ConsulAgentSelf.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value, agent)) }
end

#checks_for_node(name, dc: nil, passing: false, agent: nil) ⇒ Object



160
161
162
163
164
165
166
167
168
# File 'lib/consul/async/consul_template.rb', line 160

def checks_for_node(name, dc: nil, passing: false, agent: nil)
  raise 'You must specify a name for a service' if name.nil?

  path = "/v1/health/node/#{ERB::Util.url_encode(name.to_s)}"
  query_params = {}
  query_params[:dc] = dc if dc
  query_params[:passing] = passing if passing
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateChecks.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]', agent)) }
end

#checks_for_service(name, dc: nil, passing: false, agent: nil) ⇒ Object



149
150
151
152
153
154
155
156
157
# File 'lib/consul/async/consul_template.rb', line 149

def checks_for_service(name, dc: nil, passing: false, agent: nil)
  raise 'You must specify a name for a service' if name.nil?

  path = "/v1/health/checks/#{ERB::Util.url_encode(name.to_s)}"
  query_params = {}
  query_params[:dc] = dc if dc
  query_params[:passing] = passing if passing
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateChecks.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]', agent)) }
end

#checks_in_state(check_state, dc: nil, agent: nil) ⇒ Object



172
173
174
175
176
177
178
179
180
# File 'lib/consul/async/consul_template.rb', line 172

def checks_in_state(check_state, dc: nil, agent: nil)
  valid_checks_states = %w[any critical passing warning]
  raise "checks_in_state('#{check_state}'...) must be one of #{valid_checks_states}" unless valid_checks_states.include?(check_state)

  path = "/v1/health/state/#{check_state}"
  query_params = {}
  query_params[:dc] = dc if dc
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateChecks.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]', agent)) }
end

#create_if_missing(path, query_params, fail_fast_errors: @fail_fast_errors, max_consecutive_errors_on_endpoint: @max_consecutive_errors_on_endpoint, agent: nil, endpoint_id: nil) ⇒ Object



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/consul/async/consul_template.rb', line 403

def create_if_missing(path, query_params, fail_fast_errors: @fail_fast_errors,
                      max_consecutive_errors_on_endpoint: @max_consecutive_errors_on_endpoint,
                      agent: nil, endpoint_id: nil)
  endpoint_id ||= begin
    fqdn = path.dup
    fqdn = "#{agent}#{fqdn}"
    query_params.each_pair do |k, v|
      fqdn += "&#{k}=#{v}"
    end
    fqdn
  end
  tpl = @endpoints[endpoint_id]
  unless tpl
    tpl = yield
    ::Consul::Async::Debug.print_debug "path #{path.ljust(64)} #{query_params.inspect}\r"
    @endpoints[endpoint_id] = tpl
    tpl.endpoint.on_response do |result|
      @net_info[:success] += 1
      @net_info[:bytes_read] += result.data.bytesize
      @net_info[:changes] += 1 if result.modified?
      @net_info[:network_bytes] += result.http.response_header['Content-Length'].to_i
    end
    tpl.endpoint.on_error do |_err|
      @net_info[:errors] = @net_info[:errors] + 1
      if tpl.endpoint.stats.successes.zero? && fail_fast_errors
        ::Consul::Async::Debug.puts_error "Endpoint #{path} is failing at first call with fail fast activated, terminating..."
        terminate
      end
      if tpl.endpoint.stats.consecutive_errors > max_consecutive_errors_on_endpoint
        ::Consul::Async::Debug.puts_error "Endpoint #{path} has too many consecutive errors: #{tpl.endpoint.stats.consecutive_errors}, terminating..."
        terminate
      end
    end
  end
  tpl._seen_at(@iteration)
  tpl
end

#datacenters(agent: nil) ⇒ Object



247
248
249
250
251
# File 'lib/consul/async/consul_template.rb', line 247

def datacenters(agent: nil)
  path = '/v1/catalog/datacenters'
  query_params = {}
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateDatacenters.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]', agent)) }
end

#find_line(e) ⇒ Object



308
309
310
311
312
313
314
315
# File 'lib/consul/async/consul_template.rb', line 308

def find_line(e)
  return e.message.dup[5..] if e.message.start_with? '(erb):'

  e.backtrace.each do |line|
    return line[5..] if line.start_with? '(erb):'
  end
  nil
end

#kv(name = nil, dc: nil, keys: nil, recurse: false, agent: nil) ⇒ Object



254
255
256
257
258
259
260
261
262
# File 'lib/consul/async/consul_template.rb', line 254

def kv(name = nil, dc: nil, keys: nil, recurse: false, agent: nil)
  path = "/v1/kv/#{ERB::Util.url_encode(name.to_s)}"
  query_params = {}
  query_params[:dc] = dc if dc
  query_params[:recurse] = recurse if recurse
  query_params[:keys] = keys if keys
  default_value = '[]'
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateKV.new(ConsulEndpoint.new(consul_conf, path, true, query_params, default_value, agent), name) }
end

#node(name_or_id, dc: nil, agent: nil) ⇒ Object



191
192
193
194
195
196
# File 'lib/consul/async/consul_template.rb', line 191

def node(name_or_id, dc: nil, agent: nil)
  path = "/v1/catalog/node/#{ERB::Util.url_encode(name_or_id.to_s)}"
  query_params = {}
  query_params[:dc] = dc if dc
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateNode.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '{}', agent)) }
end

#nodes(dc: nil, agent: nil) ⇒ Object



183
184
185
186
187
188
# File 'lib/consul/async/consul_template.rb', line 183

def nodes(dc: nil, agent: nil)
  path = '/v1/catalog/nodes'
  query_params = {}
  query_params[:dc] = dc if dc
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateNodes.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]', agent)) }
end

#param(key, default_value = nil) ⇒ Object

Return a param of template



224
225
226
227
228
229
# File 'lib/consul/async/consul_template.rb', line 224

def param(key, default_value = nil)
  v = @context[:params][key]
  v ||= @context[:params][key.to_sym]
  v ||= default_value
  v
end

#render(tpl, tpl_file_path, params = {}, current_template_info: nil) ⇒ Object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/consul/async/consul_template.rb', line 317

def render(tpl, tpl_file_path, params = {}, current_template_info: nil)
  # Ugly, but allow to use render_file well to support stack of calls
  old_value = @context
  tpl_info = current_template_info.merge('source' => tpl_file_path.freeze)
  @context = {
    current_erb_path: tpl_file_path,
    params: params,
    template_info: tpl_info
  }
  result = ERB.new(tpl, trim_mode: @trim_mode).result(binding)
  raise "Result is not a string :='#{result}' for #{tpl_file_path}" unless result.is_a?(String)

  @context = old_value
  result
rescue StandardError => e
  e2 = InvalidTemplateException.new e
  raise e2, "[TEMPLATE EVALUATION ERROR] #{tpl_file_path}#{find_line(e)}: #{e.message}"
rescue SyntaxError => e
  e2 = SyntaxErrorInTemplate.new e
  raise e2, "[TEMPLATE SYNTAX ERROR] #{tpl_file_path}#{find_line(e)}: #{e.message}"
end

#render_file(path, params = {}) ⇒ Object

render a relative file with the given params accessible from template



292
293
294
295
296
297
# File 'lib/consul/async/consul_template.rb', line 292

def render_file(path, params = {})
  new_path = File.expand_path(path, File.dirname(@context[:current_erb_path]))
  raise "render_file ERROR: #{path} is resolved as #{new_path}, but the file does not exists" unless File.exist? new_path

  render(File.read(new_path), new_path, params, current_template_info: template_info)
end

#render_from_string(template_content, params = {}) ⇒ Object

render a sub template from a string template



300
301
302
303
304
305
306
# File 'lib/consul/async/consul_template.rb', line 300

def render_from_string(template_content, params = {})
  return unless template_content

  sha1res = Digest::SHA1.hexdigest(template_content)
  new_path = File.expand_path(":memory:sha1:#{sha1res}", File.dirname(@context[:current_erb_path]))
  render(template_content, new_path, params, current_template_info: template_info)
end

#secret(path = '', post_data = nil, agent: nil) ⇒ Object



277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/consul/async/consul_template.rb', line 277

def secret(path = '', post_data = nil, agent: nil)
  raise "You need to provide a vault token to use 'secret' keyword" if vault_conf.token.nil?

  path = "/v1/#{path}".gsub(%r{/{2,}}, '/')
  query_params = {}
  method = post_data ? 'POST' : 'GET'
  create_if_missing(path, query_params,
                    fail_fast_errors: vault_conf.fail_fast_errors,
                    max_consecutive_errors_on_endpoint: vault_conf.max_consecutive_errors_on_endpoint,
                    agent: agent) do
    ConsulTemplateVaultSecret.new(VaultEndpoint.new(vault_conf, path, method, true, query_params, JSON.generate(data: {}), agent: agent))
  end
end

#secrets(path = '', agent: nil) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/consul/async/consul_template.rb', line 264

def secrets(path = '', agent: nil)
  raise "You need to provide a vault token to use 'secret' keyword" if vault_conf.token.nil?

  path = "/v1/#{path}".gsub(%r{/{2,}}, '/')
  query_params = { list: 'true' }
  create_if_missing(path, query_params,
                    fail_fast_errors: vault_conf.fail_fast_errors,
                    max_consecutive_errors_on_endpoint: vault_conf.max_consecutive_errors_on_endpoint,
                    agent: agent) do
    ConsulTemplateVaultSecretList.new(VaultEndpoint.new(vault_conf, path, 'GET', true, query_params, JSON.generate(data: { keys: [] }), agent: agent))
  end
end

#service(name, dc: nil, passing: false, tag: nil, agent: nil) ⇒ Object



137
138
139
140
141
142
143
144
145
146
# File 'lib/consul/async/consul_template.rb', line 137

def service(name, dc: nil, passing: false, tag: nil, agent: nil)
  raise 'You must specify a name for a service' if name.nil?

  path = "/v1/health/service/#{ERB::Util.url_encode(name.to_s)}"
  query_params = {}
  query_params[:dc] = dc if dc
  query_params[:passing] = passing if passing
  query_params[:tag] = tag if tag
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateService.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '[]', agent)) }
end

#services(dc: nil, tag: nil, agent: nil) ⇒ Object



237
238
239
240
241
242
243
244
# File 'lib/consul/async/consul_template.rb', line 237

def services(dc: nil, tag: nil, agent: nil)
  path = '/v1/catalog/services'
  query_params = {}
  query_params[:dc] = dc if dc
  # Tag filtering is performed on client side
  query_params[:tag] = tag if tag
  create_if_missing(path, query_params, agent: agent) { ConsulTemplateServices.new(ConsulEndpoint.new(consul_conf, path, true, query_params, '{}', agent)) }
end

#template_infoObject

Get information about current template



232
233
234
# File 'lib/consul/async/consul_template.rb', line 232

def template_info
  @context[:template_info]
end

#terminateObject



389
390
391
392
393
394
395
# File 'lib/consul/async/consul_template.rb', line 389

def terminate
  @running = false
  @endpoints.each_value do |v|
    v.endpoint.terminate
  end
  @endpoints = {}
end

#vault_setup_token_renewObject



397
398
399
400
401
# File 'lib/consul/async/consul_template.rb', line 397

def vault_setup_token_renew
  path = 'v1/auth/token/renew-self'
  ::Consul::Async::Debug.print_debug 'Setting up vault token renewal'
  VaultEndpoint.new(vault_conf, path, :POST, {}, {})
end

#write(file, tpl, last_result, tpl_file_path, params = {}, current_template_info: {}) ⇒ Object



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/consul/async/consul_template.rb', line 339

def write(file, tpl, last_result, tpl_file_path, params = {}, current_template_info: {})
  @iteration = Time.now.utc - @start_time
  data = render(tpl, tpl_file_path, params, current_template_info: current_template_info)
  not_ready = []
  ready = 0
  to_cleanup = []
  @endpoints.each_pair do |endpoint_key, endpt|
    if endpt.ready?
      ready += 1
    else
      # We consider only the endpoints usefull with current iteration
      not_ready << endpoint_key unless endpt.seen_at < @iteration
    end
    to_cleanup << endpoint_key if (@iteration - endpt.seen_at) > 60
  end
  if not_ready.count.positive? || data.nil?
    if @iteration - @last_debug_time > 1
      @last_debug_time = @iteration
      if data.nil?
        ::Consul::Async::Debug.print_info "Waiting for Template #{tpl_file_path} to not return nil, consider it not ready...\r"
      else
        ::Consul::Async::Debug.print_info "Waiting for data from #{not_ready.count}/#{not_ready.count + ready} endpoints: #{not_ready[0..2]}...\r"
      end
    end
    return [false, false, nil]
  end
  if to_cleanup.count > 1
    ::Consul::Async::Debug.puts_info "Cleaned up #{to_cleanup.count} endpoints: #{to_cleanup}"
    to_cleanup.each do |to_remove|
      x = @endpoints.delete(to_remove)
      x.endpoint.terminate
    end
  end
  if last_result != data
    ::Consul::Async::Debug.print_info "Write #{Utilities.bytes_to_h data.bytesize} bytes to #{file}, "\
                 "netinfo=#{@net_info} aka "\
                 "#{Utilities.bytes_to_h((net_info[:network_bytes] / (Time.now.utc - @start_time)).round(1))}/s ...\r"
    tmp_file = "#{file}.tmp"
    begin
      File.open(tmp_file, 'w') do |f|
        f.write data
      end
      File.rename(tmp_file, file)
    rescue StandardError => e
      ::Consul::Async::Debug.puts_error "Failed  writting #{Utilities.bytes_to_h data.bytesize} bytes to #{file}: #{e.class}, message: #{e.inspect}"
    end
  end
  [true, data != last_result, data]
end