1# frozen_string_literal: true
2
3module Prometheus
4  class ProxyVariableSubstitutionService < BaseService
5    include Stepable
6
7    VARIABLE_INTERPOLATION_REGEX = /
8      {{                  # Variable needs to be wrapped in these chars.
9        \s*               # Allow whitespace before and after the variable name.
10          (?<variable>    # Named capture.
11            \w+           # Match one or more word characters.
12          )
13        \s*
14      }}
15    /x.freeze
16
17    steps :validate_variables,
18      :add_params_to_result,
19      :substitute_params,
20      :substitute_variables
21
22    # @param environment [Environment]
23    # @param params [Hash<Symbol,Any>]
24    # @param params - query [String] The Prometheus query string.
25    # @param params - start [String] (optional) A time string in the rfc3339 format.
26    # @param params - start_time [String] (optional) A time string in the rfc3339 format.
27    # @param params - end [String] (optional) A time string in the rfc3339 format.
28    # @param params - end_time [String] (optional) A time string in the rfc3339 format.
29    # @param params - variables [ActionController::Parameters] (optional) Variables with their values.
30    #     The keys in the Hash should be the name of the variable. The value should be the value of the
31    #     variable. Ex: `ActionController::Parameters.new(variable1: 'value 1', variable2: 'value 2').permit!`
32    # @return [Prometheus::ProxyVariableSubstitutionService]
33    #
34    # Example:
35    #      Prometheus::ProxyVariableSubstitutionService.new(environment, {
36    #        params: {
37    #          start_time: '2020-07-03T06:08:36Z',
38    #          end_time: '2020-07-03T14:08:52Z',
39    #          query: 'up{instance="{{instance}}"}',
40    #          variables: { instance: 'srv1' }
41    #        }
42    #      })
43    def initialize(environment, params = {})
44      @environment = environment
45      @params = params.deep_dup
46    end
47
48    # @return - params [Hash<Symbol,Any>] Returns a Hash containing a params key which is
49    #   similar to the `params` that is passed to the initialize method with 2 differences:
50    #     1. Variables in the query string are substituted with their values.
51    #        If a variable present in the query string has no known value (values
52    #        are obtained from the `variables` Hash in `params` or from
53    #        `Gitlab::Prometheus::QueryVariables.call`), it will not be substituted.
54    #     2. `start` and `end` keys are added, with their values copied from `start_time`
55    #        and `end_time`.
56    #
57    # Example output:
58    #
59    # {
60    #   params: {
61    #     start_time: '2020-07-03T06:08:36Z',
62    #     start: '2020-07-03T06:08:36Z',
63    #     end_time: '2020-07-03T14:08:52Z',
64    #     end: '2020-07-03T14:08:52Z',
65    #     query: 'up{instance="srv1"}',
66    #     variables: { instance: 'srv1' }
67    #   }
68    # }
69    def execute
70      execute_steps
71    end
72
73    private
74
75    def validate_variables(_result)
76      return success unless variables
77
78      unless variables.is_a?(ActionController::Parameters)
79        return error(_('Optional parameter "variables" must be a Hash. Ex: variables[key1]=value1'))
80      end
81
82      success
83    end
84
85    def add_params_to_result(result)
86      result[:params] = params
87
88      success(result)
89    end
90
91    def substitute_params(result)
92      start_time = result[:params][:start_time]
93      end_time = result[:params][:end_time]
94
95      result[:params][:start] = start_time if start_time
96      result[:params][:end]   = end_time if end_time
97
98      success(result)
99    end
100
101    def substitute_variables(result)
102      return success(result) unless query(result)
103
104      result[:params][:query] = gsub(query(result), full_context(result))
105
106      success(result)
107    end
108
109    def gsub(string, context)
110      # Search for variables of the form `{{variable}}` in the string and replace
111      # them with their value.
112      string.gsub(VARIABLE_INTERPOLATION_REGEX) do |match|
113        # Replace with the value of the variable, or if there is no such variable,
114        # replace the invalid variable with itself. So,
115        # `up{instance="{{invalid_variable}}"}` will remain
116        # `up{instance="{{invalid_variable}}"}` after substitution.
117        context.fetch($~[:variable], match)
118      end
119    end
120
121    def predefined_context(result)
122      Gitlab::Prometheus::QueryVariables.call(
123        @environment,
124        start_time: start_timestamp(result),
125        end_time: end_timestamp(result)
126      ).stringify_keys
127    end
128
129    def full_context(result)
130      @full_context ||= predefined_context(result).reverse_merge(variables_hash)
131    end
132
133    def variables
134      params[:variables]
135    end
136
137    def variables_hash
138      variables.to_h
139    end
140
141    def start_timestamp(result)
142      Time.rfc3339(result[:params][:start])
143    rescue ArgumentError
144    end
145
146    def end_timestamp(result)
147      Time.rfc3339(result[:params][:end])
148    rescue ArgumentError
149    end
150
151    def query(result)
152      result[:params][:query]
153    end
154  end
155end
156