Skip to content

Commit

Permalink
Merge pull request #238 from alexjfisher/remote_pql_query
Browse files Browse the repository at this point in the history
Add `remote_pql_query` function
  • Loading branch information
alexjfisher authored Jan 10, 2025
2 parents a37e3e5 + 4c11bf3 commit 1fbe9cc
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 2 deletions.
6 changes: 4 additions & 2 deletions .sync.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
---
.travis.yml:
secure: "IkrfAnec7ovZLMvhvXt8ZihyYdAJTC/nm7KDm4u2G/uD2NGaMdHNOAenkwIwC1vfCzHKcgC5u/lAYFrYvHpQpJW0kHLKnk1SpndfWX9kd5SlDDzEP5mJGjMZeTY6H9sV5fsB6Pt7l/sw5ACL/0bFDl0mYBnVhGv6UxZZ5xMQIUw="
Gemfile:
optional:
':test':
- gem: 'puppetdb-ruby'
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ group :test do
gem 'coveralls', :require => false
gem 'simplecov-console', :require => false
gem 'puppet_metadata', '~> 4.0', :require => false
gem 'puppetdb-ruby', :require => false
end

group :development do
Expand Down
96 changes: 96 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Thus making it directly usable with the values from facter.
* [`extlib::path_join`](#extlib--path_join): Take one or more paths and join them together
* [`extlib::random_password`](#extlib--random_password): A function to return a string of arbitrary length that contains randomly selected characters.
* [`extlib::read_url`](#extlib--read_url): Fetch a string from a URL (should only be used with 'small' remote files). This function should only be used with trusted/internal sources.
* [`extlib::remote_pql_query`](#extlib--remote_pql_query): Perform a PuppetDB query on an arbitrary PuppetDB server If you need to query a PuppetDB server that is not connected to your Puppet Server
* [`extlib::resources_deep_merge`](#extlib--resources_deep_merge): Deeply merge a "defaults" hash into a "resources" hash like the ones expected by `create_resources()`.
* [`extlib::sort_by_version`](#extlib--sort_by_version): A function that sorts an array of version numbers.
* [`extlib::to_ini`](#extlib--to_ini): This converts a puppet hash to an INI string.
Expand Down Expand Up @@ -958,6 +959,101 @@ Data type: `Stdlib::HTTPUrl`

The URL to read from

### <a name="extlib--remote_pql_query"></a>`extlib::remote_pql_query`

Type: Ruby 4.x API

Perform a PuppetDB query on an arbitrary PuppetDB server

If you need to query a PuppetDB server that is not connected to your Puppet
Server (perhaps part of a separate Puppet installation that uses its own
PKI), then this function is for you!

The `puppetdb-ruby` gem _must_ be installed in your puppetserver's ruby
environment before you can use this function!

#### `extlib::remote_pql_query(String[1] $query, HTTPSUrl $url, String[1] $key, String[1] $cert, String[1] $cacert, Optional[Hash] $options)`

The extlib::remote_pql_query function.

Returns: `Array` Returns the PQL query response results

##### `query`

Data type: `String[1]`

The PQL query to run

##### `url`

Data type: `HTTPSUrl`

The PuppetDB HTTPS URL (SSL with cert-based authentication)

##### `key`

Data type: `String[1]`

The client SSL key associated with the SSL client certificate

##### `cert`

Data type: `String[1]`

The client SSL cert to present to PuppetDB

##### `cacert`

Data type: `String[1]`

The CA certificate

##### `options`

Data type: `Optional[Hash]`

PuppetDB query options. (See https://www.puppet.com/docs/puppetdb/8/api/query/v4/paging)

#### `extlib::remote_pql_query(String[1] $query, HTTPUrl $url, Optional[Hash] $options)`

The extlib::remote_pql_query function.

Returns: `Array` Returns the PQL query response results

##### Examples

###### 'Collecting' exported resource defined type from a foreign PuppetDB

```puppet
$pql_results = extlib::remote_pql_query(
"resources[title,parameters] { type = \"My_Module::My_type\" and nodes { deactivated is null } and exported = true and parameters.collect_on = \"${trusted['certname']}\" }",
'http://puppetdb.example.com:8080',
)
$pql_results.each |$result| {
my_module::my_type { $result['title']:
* => $result['parameters']
}
}
```

##### `query`

Data type: `String[1]`

The PQL query to run

##### `url`

Data type: `HTTPUrl`

The PuppetDB HTTP URL (non SSL version)

##### `options`

Data type: `Optional[Hash]`

PuppetDB query options. (See https://www.puppet.com/docs/puppetdb/8/api/query/v4/paging)

### <a name="extlib--resources_deep_merge"></a>`extlib::resources_deep_merge`

Type: Ruby 4.x API
Expand Down
123 changes: 123 additions & 0 deletions lib/puppet/functions/extlib/remote_pql_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# frozen_string_literal: true

require 'tempfile'

# Perform a PuppetDB query on an arbitrary PuppetDB server
#
# If you need to query a PuppetDB server that is not connected to your Puppet
# Server (perhaps part of a separate Puppet installation that uses its own
# PKI), then this function is for you!
#
# The `puppetdb-ruby` gem _must_ be installed in your puppetserver's ruby
# environment before you can use this function!
Puppet::Functions.create_function(:'extlib::remote_pql_query') do
local_types do
type 'HTTPUrl = Pattern[/(?i:\Ahttp:\/\/.*\z)/]'
type 'HTTPSUrl = Pattern[/(?i:\Ahttps:\/\/.*\z)/]'
end

# @param query The PQL query to run
# @param url The PuppetDB HTTPS URL (SSL with cert-based authentication)
# @param key The client SSL key associated with the SSL client certificate
# @param cert The client SSL cert to present to PuppetDB
# @param cacert The CA certificate
# @param options PuppetDB query options. (See https://www.puppet.com/docs/puppetdb/8/api/query/v4/paging)
# @return Returns the PQL query response results
dispatch :secure_remote_pql_query do
param 'String[1]', :query
param 'HTTPSUrl', :url
param 'String[1]', :key
param 'String[1]', :cert
param 'String[1]', :cacert
optional_param 'Hash', :options
return_type 'Array'
end

# @param query The PQL query to run
# @param url The PuppetDB HTTP URL (non SSL version)
# @param options PuppetDB query options. (See https://www.puppet.com/docs/puppetdb/8/api/query/v4/paging)
# @return Returns the PQL query response results
# @example 'Collecting' exported resource defined type from a foreign PuppetDB
# $pql_results = extlib::remote_pql_query(
# "resources[title,parameters] { type = \"My_Module::My_type\" and nodes { deactivated is null } and exported = true and parameters.collect_on = \"${trusted['certname']}\" }",
# 'http://puppetdb.example.com:8080',
# )
# $pql_results.each |$result| {
# my_module::my_type { $result['title']:
# * => $result['parameters']
# }
# }
dispatch :insecure_remote_pql_query do
param 'String[1]', :query
param 'HTTPUrl', :url
optional_param 'Hash', :options
return_type 'Array'
end

def secure_remote_pql_query(query, url, key, cert, cacert, options = {})
keyfile = Tempfile.new('remote_pql_query_keyfile')
certfile = Tempfile.new('remote_pql_query_certfile')
cafile = Tempfile.new('remote_pql_query_cafile')

begin
keyfile.write(key)
keyfile.close

certfile.write(cert)
certfile.close

cafile.write(cacert)
cafile.close

client_options = {
server: url,
pem: {
'key' => keyfile.path,
'cert' => certfile.path,
'ca_file' => cafile.path,
}
}

remote_pql_query(query, options, client_options)
ensure
[keyfile, certfile, cafile].each(&:unlink)
end
end

def insecure_remote_pql_query(query, url, options = {})
client_options = { server: url }

remote_pql_query(query, options, client_options)
end

def remote_pql_query(query, query_options, client_options)
require 'puppetdb'

# If the dalen/puppetdbquery module is installed, then there'll be a clash
# of libraries/namespaces and we need to manually require the files from
# puppetdb-ruby...
unless PuppetDB.constants.include?(:Client)
require 'puppetdb/client'
require 'puppetdb/query'
require 'puppetdb/response'
require 'puppetdb/error'
require 'puppetdb/config'
end

client = PuppetDB::Client.new(client_options)

begin
response = client.request(
'', # PQL
query,
query_options
)

response.data
rescue PuppetDB::APIError => e
raise Puppet::Error, "PuppetDB API Error: #{e.response.inspect}"
rescue StandardError => e
raise Puppet::Error, "Remote PQL query failed: #{e.message}"
end
end
end
104 changes: 104 additions & 0 deletions spec/functions/extlib/remote_pql_query_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

require 'spec_helper'
require 'puppetdb'

describe 'extlib::remote_pql_query' do
let(:mock_client) { instance_double(PuppetDB::Client) }
let(:mock_response) { PuppetDB::Response.new(['test_result']) }

before do
allow(PuppetDB::Client).to receive(:new).and_return(mock_client)
allow(mock_client).to receive(:request).and_return(mock_response)
end

context 'secure_remote_pql_query' do
it 'returns the data array for valid HTTPS params' do
is_expected.to run.with_params(
'facts { name = "osfamily" }', # query
'https://puppetdb.example.com', # URL (matches HTTPS dispatch)
'client_key', # key
'client_cert', # cert
'ca_cert' # cacert
).and_return(['test_result'])
end

it 'raises ArgumentError if given an HTTP URL in the secure dispatch' do
is_expected.to run.with_params(
'facts { name = "osfamily" }',
'http://puppetdb.example.com', # Wrong for secure dispatch
'client_key',
'client_cert',
'ca_cert'
).and_raise_error(
ArgumentError, %r{parameter 'url'}i
)
end
end

context 'insecure_remote_pql_query' do
it 'returns the data array for valid HTTP params' do
is_expected.to run.with_params(
'facts { name = "osfamily" }', # query
'http://puppetdb.example.com' # URL (matches HTTP dispatch)
).and_return(['test_result'])
end

it 'raises ArgumentError if given an HTTPS URL in the insecure dispatch' do
is_expected.to run.with_params(
'facts { name = "osfamily" }',
'https://puppetdb.example.com' # Wrong for insecure dispatch
).and_raise_error(
ArgumentError, %r{parameter 'url'}i
)
end
end

context 'with query options' do
it 'passes options to the client.request call' do
allow(mock_client).to receive(:request).with(
'',
'resources { type = "File" }',
{ 'limit' => 5 }
).and_return(mock_response)

is_expected.to run.with_params(
'resources { type = "File" }',
'http://puppetdb.example.com',
{ 'limit' => 5 }
).and_return(['test_result'])

expect(mock_client).to have_received(:request).with(
'',
'resources { type = "File" }',
{ 'limit' => 5 }
)
end
end

context 'when PuppetDB::APIError is raised' do
it 're-raises as a Puppet::Error' do
allow(mock_client).to receive(:request).and_raise(
PuppetDB::APIError.new(
instance_double(PuppetDB::Response, inspect: 'some API error')
)
)

is_expected.to run.with_params(
'facts { name = "osfamily" }',
'http://puppetdb.example.com'
).and_raise_error(Puppet::Error, %r{PuppetDB API Error: some API error})
end
end

context 'when a generic error is raised' do
it 're-raises as a Puppet::Error' do
allow(mock_client).to receive(:request).and_raise(RuntimeError, 'boom')

is_expected.to run.with_params(
'facts { name = "osfamily" }',
'http://puppetdb.example.com'
).and_raise_error(Puppet::Error, %r{Remote PQL query failed: boom})
end
end
end

0 comments on commit 1fbe9cc

Please sign in to comment.