# frozen_string_literal: true require 'config_server_agent/version' require 'net/http' require 'json' require 'digest/sha1' require 'lockfile' class ConfigServerAgent class Error < StandardError; end def initialize( auth0_client_id: ENV['AUTH0_CLIENT_ID'], auth0_client_secret: ENV['AUTH0_CLIENT_SECRET'], auth0_host: ENV['AUTH0_HOST'], config_server_audience: ENV['CONFIG_SERVER_AUDIENCE'], config_server_api_key: ENV['CONFIG_SERVER_API_KEY'], config_server_host: ENV['CONFIG_SERVER_HOST'], user_agent: "ConfigServerAgent/#{ConfigServerAgent::VERSION}", user_agent_comment: nil, token_ttl: nil, use_cache: false, cache_file: nil ) @auth0_client_id = auth0_client_id or raise ArgumentError, 'Missing auth0_client_id parameter' @auth0_client_secret = auth0_client_secret or raise ArgumentError, 'Missing auth0_client_secret parameter' @auth0_host = auth0_host or raise ArgumentError, 'Missing auth0_host parameter' @config_server_audience = config_server_audience or raise ArgumentError, 'Missing config_server_audience parameter' @config_server_api_key = config_server_api_key or raise ArgumentError, 'Missing config_server_api_key parameter' @config_server_host = config_server_host or raise ArgumentError, 'Missing config_server_host parameter' @config = nil @mutex = Mutex.new @user_agent = user_agent @user_agent += " (#{user_agent_comment})" if user_agent_comment @token_expires = nil @token = nil @token_ttl = token_ttl @token_buffer = 10 # seconds @use_cache = use_cache @cache_file = cache_file || '/tmp/' + Digest::SHA1.hexdigest("#{auth0_client_id}/#{ConfigServerAgent::VERSION}") + '.json' end def get_config return @config if @config @mutex.synchronize do @config ||= get_request 'config_pack' end end def get_selected_config(area, item=nil, **options) if options[:ignore_cache] @config = nil end data = get_config.group_by { |x| x['area'] } raise Error, "Area is missing: #{area}" unless data.key? area if item data.fetch(area, []).find { |row_item| break row_item["value"] if row_item["name"] == item } else data.fetch(area, []) end end def set_config(values) raise Error, 'values not found' if values.empty? post_request('set_values', values) end def clear @config = nil end def notify_missing(area, missing) post_request 'notify_missing', area: area, name: missing end private def post_request(endpoint, data={}) dispatch_request(endpoint) do |url| request = Net::HTTP::Post.new url request.body = data.to_json request end end def get_request(endpoint, data={}) dispatch_request(endpoint) do |url| url.query = URI.encode_www_form data Net::HTTP::Get.new url end end def dispatch_request(endpoint) url = URI "#{@config_server_host}/api/#{endpoint}" http = Net::HTTP.new url.host, url.port http.use_ssl = url.scheme == 'https' request = yield url request['user-agent'] = @user_agent request['authorization'] = "Bearer #{get_token}" request['content-type'] = 'application/json' request['accept'] = 'application/json' request['api-key'] = @config_server_api_key process_response http.request(request) end def process_response(response) unless response['content-type'].start_with? 'application/json' raise Error, "Unexpected content-type from server: #{response['content-type']}" end data = JSON.parse response.read_body raise Error, "Config Server error: #{data['error']}" unless response.is_a? Net::HTTPSuccess data rescue JSON::ParserError raise Error, "Invalid JSON received from #{@config_server_host}" end def get_token if @token.nil? and @use_cache read_token_cache end if @token.nil? or (@token_expires and @token_expires < Time.now.to_i) request_new_token write_token_cache if @use_cache end @token end def request_new_token url = URI "https://#{@auth0_host}/oauth/token" http = Net::HTTP.new url.host, url.port http.use_ssl = url.scheme == 'https' request = Net::HTTP::Post.new url request['user-agent'] = @user_agent request['content-type'] = 'application/json' request.body = { client_id: @auth0_client_id, client_secret: @auth0_client_secret, audience: @config_server_audience, grant_type: 'client_credentials', }.to_json response = JSON.parse http.request(request).read_body @token = response['access_token'] or raise Error, "No token from #{@auth0_host}" @token_expires = Time.now.to_i + (@token_ttl || response['expires_in']) - @token_buffer @token rescue JSON::ParserError raise Error, "Invalid JSON received from #{@auth0_host}" end def read_token_cache with_lock do @token, @token_expires = JSON.parse(File.read @cache_file).values_at 'token', 'token_expires' end if File.file? @cache_file end def write_token_cache with_lock do File.write(@cache_file, { 'token' => @token, 'token_expires' => @token_expires }.to_json) # Check if o+wr bits are set on the cache file if File.stat(@cache_file).mode & 06 > 0 begin # Attempt to remove them if they are... File.chmod 0660, @cache_file rescue Errno::EPERM => e # But don't raise an error if it fails because the file may have been created by another user warn "File permissions are too open: #{@cache_file} - #{e.message}" end end end end def with_lock Lockfile.new("/var/lock/config_server_agent.rb.lock", retries: 3, poll_retries: 3) do yield end end end