From 313c16cfcb5f9989d5b29b7e25d5c093c64e3f71 Mon Sep 17 00:00:00 2001 From: Christoph Maser Date: Mon, 26 Jan 2026 18:11:01 +0100 Subject: [PATCH] feat: allow to pass additional HTTP headers in puppetdb requests This can be useful if you want to access PuppetDB via custom authentication. Eg if you want to access PuppetDB behind an API gateway that requires oauth tokens or other custom headers. --- documentation/bolt_connect_puppetdb.md | 21 +++++++-- lib/bolt/config/options.rb | 6 +++ lib/bolt/puppetdb/config.rb | 8 ++++ lib/bolt/puppetdb/instance.rb | 1 + schemas/bolt-defaults.schema.json | 22 +++++++++ schemas/bolt-project.schema.json | 22 +++++++++ spec/unit/puppetdb/config_spec.rb | 21 +++++++++ spec/unit/puppetdb/instance_spec.rb | 65 ++++++++++++++++++++++++++ 8 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 spec/unit/puppetdb/instance_spec.rb diff --git a/documentation/bolt_connect_puppetdb.md b/documentation/bolt_connect_puppetdb.md index 62ee0b428..b7dfacb4d 100644 --- a/documentation/bolt_connect_puppetdb.md +++ b/documentation/bolt_connect_puppetdb.md @@ -50,6 +50,7 @@ config](configuring_bolt.md) with the following values: | --- | --- | --- | | `cacert` | `String` | The path to the CA certificate for PuppetDB. | | `connect_timeout` | `Integer` | How long to wait in seconds when establishing connections with PuppetDB. | +| `headers` | `Hash` | A map of HTTP headers to add to PuppetDB requests. | | `read_timeout` | `Integer` | How long to wait in seconds for a response from PuppetDB. | | `server_urls` | `Array` | An array of strings containing the PuppetDB host to connect to. Include the protocol `https` and the port, which is usually `8081`. For example, `https://my-puppetdb-server.example.com:8081`. The Bolt PuppetDB client attempts to connect to each host in the list until it makes a successful connection. | @@ -94,6 +95,16 @@ puppetdb: token: ~/.puppetlabs/token ``` +To use custom headers, such as for OAuth authentication: + +``` +puppetdb: + server_urls: ["https://puppet.example.com:8081"] + cacert: /etc/puppetlabs/puppet/ssl/certs/ca.pem + headers: + Authorization: "Bearer " +``` + ## Configuring multiple PuppetDB instances The Bolt PuppetDB Client supports connections to multiple PuppetDB instances. To @@ -244,15 +255,15 @@ plan puppetdb_query_targets { # this returns an array of objects, each object containing a "certname" parameter: # [ {"certname": "node1"}, {"certname": "node2"} ] $query_results = puppetdb_query("nodes[certname] {}") - + # since puppetdb_query() returns the JSON results from the API call, we need to transform this # data into Targets to use it in one of the run_*() functions. # extract the "certname" values, so now we have an array of hostnames $certnames = $query_results.map |$r| { $r['certname'] } - + # transform the arary of certnames into an array of Targets $targets = get_targets($certnames) - + # gather facts about all of the nodes run_task('facts', $targets) } @@ -270,10 +281,10 @@ plan puppetdb_plugin_targets { 'query' => 'nodes[certname] {}', } $references = resolve_references($refs) - + # maps the results into a list of Target objects $targets = $references.map |$r| { Target.new($r) } - + # gather facts about all of the nodes run_task('facts', $targets) } diff --git a/lib/bolt/config/options.rb b/lib/bolt/config/options.rb index dea2d1c87..323a358d0 100644 --- a/lib/bolt/config/options.rb +++ b/lib/bolt/config/options.rb @@ -76,6 +76,12 @@ module Options _example: 120, _plugin: true }, + "headers" => { + description: "A map of HTTP headers to add to PuppetDB requests.", + type: Hash, + _example: { "Authorization" => "Bearer " }, + _plugin: true + }, "key" => { description: "The private key for the certificate.", type: String, diff --git a/lib/bolt/puppetdb/config.rb b/lib/bolt/puppetdb/config.rb index f62795438..e0e655798 100644 --- a/lib/bolt/puppetdb/config.rb +++ b/lib/bolt/puppetdb/config.rb @@ -149,6 +149,14 @@ def key @settings['key'] end + def headers + if @settings['headers'] && !@settings['headers'].is_a?(Hash) + raise Bolt::PuppetDBError, "headers must be a Hash" + end + + @settings['headers'] + end + def validate_cert_and_key if (@settings['cert'] && !@settings['key']) || (!@settings['cert'] && @settings['key']) diff --git a/lib/bolt/puppetdb/instance.rb b/lib/bolt/puppetdb/instance.rb index 26992ae90..3f006648d 100644 --- a/lib/bolt/puppetdb/instance.rb +++ b/lib/bolt/puppetdb/instance.rb @@ -139,6 +139,7 @@ def uri def headers headers = { 'Content-Type' => 'application/json' } + headers.merge!(@config.headers) if @config.headers headers['X-Authentication'] = @config.token if @config.token headers end diff --git a/schemas/bolt-defaults.schema.json b/schemas/bolt-defaults.schema.json index caa3404cd..89676df4e 100644 --- a/schemas/bolt-defaults.schema.json +++ b/schemas/bolt-defaults.schema.json @@ -299,6 +299,17 @@ } ] }, + "headers": { + "description": "A map of HTTP headers to add to PuppetDB requests.", + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "key": { "description": "The private key for the certificate.", "oneOf": [ @@ -395,6 +406,17 @@ } ] }, + "headers": { + "description": "A map of HTTP headers to add to PuppetDB requests.", + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "key": { "description": "The private key for the certificate.", "oneOf": [ diff --git a/schemas/bolt-project.schema.json b/schemas/bolt-project.schema.json index 4714efabe..317baa22d 100644 --- a/schemas/bolt-project.schema.json +++ b/schemas/bolt-project.schema.json @@ -429,6 +429,17 @@ } ] }, + "headers": { + "description": "A map of HTTP headers to add to PuppetDB requests.", + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "key": { "description": "The private key for the certificate.", "oneOf": [ @@ -525,6 +536,17 @@ } ] }, + "headers": { + "description": "A map of HTTP headers to add to PuppetDB requests.", + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "key": { "description": "The private key for the certificate.", "oneOf": [ diff --git a/spec/unit/puppetdb/config_spec.rb b/spec/unit/puppetdb/config_spec.rb index e003b131f..37494e2ae 100644 --- a/spec/unit/puppetdb/config_spec.rb +++ b/spec/unit/puppetdb/config_spec.rb @@ -254,4 +254,25 @@ Bolt::PuppetDB::Config.new(config: {}, load_defaults: true) end end + + context 'when validating headers' do + let(:options) { { 'server_urls' => ['https://puppetdb:8081'], 'headers' => headers } } + let(:config) { Bolt::PuppetDB::Config.new(config: options) } + + context 'with valid headers' do + let(:headers) { { 'Authorization' => 'Bearer token' } } + + it 'returns the headers' do + expect(config.headers).to eq(headers) + end + end + + context 'with invalid headers' do + let(:headers) { 'Authorization: Bearer token' } + + it 'raises an error' do + expect { config.headers }.to raise_error(Bolt::PuppetDBError, "headers must be a Hash") + end + end + end end diff --git a/spec/unit/puppetdb/instance_spec.rb b/spec/unit/puppetdb/instance_spec.rb new file mode 100644 index 000000000..948e99535 --- /dev/null +++ b/spec/unit/puppetdb/instance_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'bolt/puppetdb/instance' + +describe Bolt::PuppetDB::Instance do + let(:token) { 'token' } + let(:config) do + { + 'server_urls' => ["https://puppet.example.com:8081"], + 'cacert' => '/etc/puppetlabs/puppet/ssl/certs/ca.pem', + 'token' => '~/.puppetlabs/token' + } + end + let(:instance) { described_class.new(config: config) } + + before(:each) do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with(File.expand_path('~/.puppetlabs/token')).and_return(token) + end + + context "#headers" do + it "includes Content-Type" do + expect(instance.headers).to include('Content-Type' => 'application/json') + end + + it "includes X-Authentication token" do + expect(instance.headers).to include('X-Authentication' => token) + end + + context "with custom headers" do + let(:config) do + { + 'server_urls' => ["https://puppet.example.com:8081"], + 'headers' => { 'Authorization' => 'Bearer info' } + } + end + + it "includes custom headers" do + expect(instance.headers).to include('Authorization' => 'Bearer info') + end + + it "does not include X-Authentication if no token" do + # config does not have 'token', so it falls back to DEFAULT_TOKEN. + # We need to simulate no default token file. + allow(File).to receive(:exist?).with(Bolt::PuppetDB::Config::DEFAULT_TOKEN).and_return(false) + expect(instance.headers).not_to have_key('X-Authentication') + end + end + + context "with custom headers overlapping Content-Type" do + let(:config) do + { + 'server_urls' => ["https://puppet.example.com:8081"], + 'headers' => { 'Content-Type' => 'application/x-yaml' } + } + end + + it "overrides default Content-Type" do + expect(instance.headers).to include('Content-Type' => 'application/x-yaml') + end + end + end +end