This file is indexed.

/usr/lib/ruby/vendor_ruby/octocatalog-diff/puppetdb.rb is in octocatalog-diff 1.5.3-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# frozen_string_literal: true

require_relative 'errors'
require_relative 'util/httparty'

require 'uri'

module OctocatalogDiff
  # A standard way to connect to PuppetDB from the various scripts in this repository.
  class PuppetDB
    DEFAULT_HTTPS_PORT = 8081
    DEFAULT_HTTP_PORT = 8080

    # Allow connections to be read (used in tests for now)
    attr_reader :connections

    # Constructor - will construct connection parameters from a variety
    # of sources, including arguments and environment variables. Supported
    # environment variables:
    #   PUPPETDB_URL
    #   PUPPETDB_HOST [+ PUPPETDB_PORT] [+ PUPPETDB_SSL]
    #
    # Order of precedence:
    #    1. :puppetdb_url argument (String or Array<String>)
    #    2. :puppetdb_host argument [+ :puppetdb_port] [+ :puppetdb_ssl]
    #    3. ENV['PUPPETDB_URL']
    #    4. ENV['PUPPETDB_HOST'] [+ ENV['PUPPETDB_PORT']], [+ ENV['PUPPETDB_SSL']]
    # When it finds one of these, it stops and does not process any others.
    #
    # When :puppetdb_url is an array, all given URLs are tried, in random order,
    # until a connection succeeds. If a connection succeeds, any errors from previously
    # failed connections are suppressed.
    #
    # Supported arguments:
    # @param :puppetdb_url [String or Array<String>] PuppetDB URL(s) to try in random order
    # @param :puppetdb_host [String] PuppetDB hostname, when constructing a URL
    # @param :puppetdb_port [Integer] Port number, defaults to 8080 (non-SSL) or 8081 (SSL)
    # @param :puppetdb_ssl [Boolean] defaults to true, because you should use SSL
    # @param :puppetdb_ssl_ca [String] Path to file containing CA certificate
    # @param :puppetdb_ssl_verify [Boolean] Override the CA verification setting guessed from parameters
    # @param :puppetdb_ssl_client_pem [String] PEM-encoded client key and certificate
    # @param :puppetdb_ssl_client_p12 [String] pkcs12-encoded client key and certificate
    # @param :puppetdb_ssl_client_password [String] Path to file containing password for SSL client key (any format)
    # @param :puppetdb_ssl_client_auth [Boolean] Override the client-auth that is guessed from parameters
    # @param :puppetdb_token [String] PE RBAC token to authenticate to PuppetDB API
    # @param :timeout [Integer] Connection timeout for PuppetDB (default=10)
    def initialize(options = {})
      @connections =
        if options.key?(:puppetdb_url)
          urls = options[:puppetdb_url].is_a?(Array) ? options[:puppetdb_url] : [options[:puppetdb_url]]
          urls.map { |url| parse_url(url) }
        elsif options.key?(:puppetdb_host)
          is_ssl = options.fetch(:puppetdb_ssl, true)
          default_port = is_ssl ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
          port = options.fetch(:puppetdb_port, default_port).to_i
          [{ ssl: is_ssl, host: options[:puppetdb_host], port: port }]
        elsif ENV['PUPPETDB_URL'] && !ENV['PUPPETDB_URL'].empty?
          [parse_url(ENV['PUPPETDB_URL'])]
        elsif ENV['PUPPETDB_HOST'] && !ENV['PUPPETDB_HOST'].empty?
          # Because environment variables are strings...
          # This will get the env var and see if it equals 'true'; the result
          # of this == comparison is the true/false boolean we need.
          is_ssl = ENV.fetch('PUPPETDB_SSL', 'true') == 'true'
          default_port = is_ssl ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
          port = ENV.fetch('PUPPETDB_PORT', default_port).to_i
          [{ ssl: is_ssl, host: ENV['PUPPETDB_HOST'], port: port }]
        else
          []
        end
      @timeout = options.fetch(:timeout, 10)
      @options = options
    end

    # Wrapper around the httparty call in the private _get method.
    # Returns the parsed result of getting the provided URL and returns
    # a friendlier error message if there are network connection problems
    # to PuppetDB.
    # @param path [String] Path portion of the URL
    # @return [Object] Parsed reply from PuppetDB as an object
    def get(path)
      _get(path)
    rescue Net::OpenTimeout, Errno::ECONNREFUSED => exc
      raise OctocatalogDiff::Errors::PuppetDBConnectionError, "#{exc.class} connecting to PuppetDB (need VPN on?): #{exc.message}"
    end

    private

    # HTTP(S) Query - will attempt to retrieve URL from each connection
    # @param path [String] Path portion of the URL
    # @return [String] Parsed response
    def _get(path)
      # You need at least one connection or else this can't do anything
      raise ArgumentError, 'No PuppetDB connections configured' if @connections.empty?

      # Keep track of the latest exception seen
      exc = nil

      # Try each connection in random order. This will return the first successful
      # response, and try the next connection if there's an error. Once it's out of
      # connections to try it will raise the last exception encountered.
      @connections.shuffle.each do |connection|
        complete_url = [
          connection[:ssl] ? 'https://' : 'http://',
          connection[:host],
          ':',
          connection[:port],
          path
        ].join('')

        begin
          headers = { 'Accept' => 'application/json' }
          headers['X-Authentication'] = @options[:puppetdb_token] if @options[:puppetdb_token]
          more_options = { headers: headers, timeout: @timeout }

          if connection[:username] || connection[:password]
            more_options[:basic_auth] = { username: connection[:username], password: connection[:password] }
          end
          response = OctocatalogDiff::Util::HTTParty.get(complete_url, @options.merge(more_options), 'puppetdb')

          # Handle all non-200's from PuppetDB
          unless response[:code] == 200
            raise OctocatalogDiff::Errors::PuppetDBNodeNotFoundError, "404 - #{response[:error]}" if response[:code] == 404
            raise OctocatalogDiff::Errors::PuppetDBGenericError, "#{response[:code]} - #{response[:error]}"
          end

          # PuppetDB can return 'Not Found' as a string with a 200 response code
          raise NotFoundError, '404 - Not Found' if response[:body] == 'Not Found'

          # PuppetDB can also return an error message in a 200; we'll call this a 500
          if response.key?(:error)
            raise OctocatalogDiff::Errors::PuppetDBGenericError, "500 - #{response[:error]}"
          end

          # If we get here without raising an error, it will fall out of the begin/rescue
          # with 'result' non-nil, and 'result' will then get returned.
          raise "Unparseable response from puppetdb: '#{response.inspect}'" unless response[:parsed]
          result = response[:parsed]
        rescue => exc
          # Set response to nil so the loop repeats itself if there are retries left.
          # Also sets 'exc' to the most recent exception, in case all retries are
          # exhausted and this exception has to be raised.
          result = nil
        end

        # If the previous query didn't error, return result
        return result unless result.nil?
      end

      # At this point no query has succeeded, so raise the last error encountered.
      raise exc
    end

    # Parse a URL to determine hostname, port number, and whether or not SSL is used.
    # @param url [String] URL to parse
    # @return [Hash] { ssl: true/false, host: <String>, port: <Integer> }
    def parse_url(url)
      uri = URI(url)
      if URI.split(url)[3].nil?
        uri.port = uri.scheme == 'https' ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
      end

      raise ArgumentError, "URL #{url} has invalid scheme" unless uri.scheme =~ /^https?$/
      parsed_url = { ssl: uri.scheme == 'https', host: uri.host, port: uri.port }
      if uri.user || uri.password
        parsed_url[:username] = uri.user
        parsed_url[:password] = uri.password
      end

      parsed_url
    rescue URI::InvalidURIError => exc
      raise exc.class, "Invalid URL: #{url} (#{exc.message})"
    end
  end
end