Skip to content

Commit a5411f2

Browse files
authored
Merge pull request #8556 from donoghuc/PUP-11002
(PUP-11002) Expose v4 Catalog endpoint in compiler service
2 parents 62ad648 + c3800ff commit a5411f2

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

lib/puppet/http/service/compiler.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,75 @@ def post_catalog(name, facts:, environment:, configured_environment: nil, transa
129129
[response, deserialize(response, Puppet::Resource::Catalog)]
130130
end
131131

132+
#
133+
# @api private
134+
#
135+
# Submit a POST request to request a catalog to the server using v4 endpoint
136+
#
137+
# @param [String] certname The name of the node for which to compile the catalog.
138+
# @param [Hash] persistent A hash containing two required keys, facts and catalog,
139+
# which when set to true will cause the facts and reports to be stored in
140+
# PuppetDB, or discarded if set to false.
141+
# @param [String] environment The name of the environment for which to compile the catalog.
142+
# @param [Hash] facts A hash with a required values key, containing a hash of all the
143+
# facts for the node. If not provided, Puppet will attempt to fetch facts for the node
144+
# from PuppetDB.
145+
# @param [Hash] trusted_facts A hash with a required values key containing a hash of
146+
# the trusted facts for a node
147+
# @param [String] transaction_uuid The id for tracking the catalog compilation and
148+
# report submission.
149+
# @param [String] job_id The id of the orchestrator job that triggered this run.
150+
# @param [Hash] options A hash of options beyond direct input to catalogs. Options:
151+
# - prefer_requested_environment Whether to always override a node's classified
152+
# environment with the one supplied in the request. If this is true and no environment
153+
# is supplied, fall back to the classified environment, or finally, 'production'.
154+
# - capture_logs Whether to return the errors and warnings that occurred during
155+
# compilation alongside the catalog in the response body.
156+
# - log_level The logging level to use during the compile when capture_logs is true.
157+
# Options are 'err', 'warning', 'info', and 'debug'.
158+
#
159+
# @return [Array<Puppet::HTTP::Response, Puppet::Resource::Catalog, Array<String>>] An array
160+
# containing the request response, the deserialized catalog returned by
161+
# the server and array containing logs (log array will be empty if capture_logs is false)
162+
#
163+
def post_catalog4(certname, persistence:, environment:, facts: nil, trusted_facts: nil, transaction_uuid: nil, job_id: nil, options: nil)
164+
unless persistence.is_a?(Hash) && (missing = [:facts, :catalog] - persistence.keys.map(&:to_sym)).empty?
165+
raise ArgumentError.new("The 'persistence' hash is missing the keys: #{missing.join(', ')}")
166+
end
167+
raise ArgumentError.new("Facts must be a Hash not a #{facts.class}") unless facts.nil? || facts.is_a?(Hash)
168+
body = {
169+
certname: certname,
170+
persistence: persistence,
171+
environment: environment,
172+
transaction_uuid: transaction_uuid,
173+
job_id: job_id,
174+
options: options
175+
}
176+
body[:facts] = { values: facts } unless facts.nil?
177+
body[:trusted_facts] = { values: trusted_facts } unless trusted_facts.nil?
178+
headers = add_puppet_headers(
179+
'Accept' => get_mime_types(Puppet::Resource::Catalog).join(', '),
180+
'Content-Type' => 'application/json'
181+
)
182+
183+
url = URI::HTTPS.build(host: @url.host, port: @url.port, path: Puppet::Util.uri_encode("/puppet/v4/catalog"))
184+
response = @client.post(
185+
url,
186+
body.to_json,
187+
headers: headers
188+
)
189+
process_response(response)
190+
begin
191+
response_body = JSON.parse(response.body)
192+
catalog = Puppet::Resource::Catalog.from_data_hash(response_body['catalog'])
193+
rescue => err
194+
raise Puppet::HTTP::SerializationError.new("Failed to deserialize catalog from puppetserver response: #{err.message}", err)
195+
end
196+
197+
logs = response_body['logs'] || []
198+
[response, catalog, logs]
199+
end
200+
132201
#
133202
# @api private
134203
#

spec/unit/http/service/compiler_spec.rb

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# coding: utf-8
23
require 'spec_helper'
34
require 'puppet/http'
@@ -258,6 +259,128 @@
258259
end
259260
end
260261

262+
context 'when posting for a v4 catalog' do
263+
let(:uri) {"https://compiler.example.com:8140/puppet/v4/catalog"}
264+
let(:persistence) {{ facts: true, catalog: true }}
265+
let(:facts) {{ 'foo' => 'bar' }}
266+
let(:trusted_facts) {{}}
267+
let(:uuid) { "ec3d2844-b236-4287-b0ad-632fbb4d1ff0" }
268+
let(:job_id) { "1" }
269+
let(:payload) {{
270+
environment: environment,
271+
persistence: persistence,
272+
facts: facts,
273+
trusted_facts: trusted_facts,
274+
transaction_uuid: uuid,
275+
job_id: job_id,
276+
options: {
277+
prefer_requested_environment: false,
278+
capture_logs: false
279+
}
280+
}}
281+
let(:serialized_catalog) {{ 'catalog' => catalog.to_data_hash }.to_json}
282+
let(:catalog_response) {{ body: serialized_catalog, headers: {'Content-Type' => formatter.mime }}}
283+
284+
it 'includes default HTTP headers' do
285+
stub_request(:post, uri).with do |request|
286+
expect(request.headers).to include({'X-Puppet-Version' => /./, 'User-Agent' => /./})
287+
expect(request.headers).to_not include('X-Puppet-Profiling')
288+
end.to_return(**catalog_response)
289+
290+
subject.post_catalog4(certname, payload)
291+
end
292+
293+
it 'defaults the server and port based on settings' do
294+
Puppet[:server] = 'compiler2.example.com'
295+
Puppet[:serverport] = 8141
296+
297+
stub_request(:post, "https://compiler2.example.com:8141/puppet/v4/catalog")
298+
.to_return(**catalog_response)
299+
300+
subject.post_catalog4(certname, payload)
301+
end
302+
303+
it 'includes puppet headers set via the :http_extra_headers and :profile settings' do
304+
stub_request(:post, uri).with(headers: {'Example-Header' => 'real-thing', 'another' => 'thing', 'X-Puppet-Profiling' => 'true'}).
305+
to_return(**catalog_response)
306+
307+
Puppet[:http_extra_headers] = 'Example-Header:real-thing,another:thing'
308+
Puppet[:profile] = true
309+
310+
subject.post_catalog4(certname, payload)
311+
end
312+
313+
it 'returns a deserialized catalog' do
314+
stub_request(:post, uri)
315+
.to_return(**catalog_response)
316+
317+
_, cat, _ = subject.post_catalog4(certname, payload)
318+
expect(cat).to be_a(Puppet::Resource::Catalog)
319+
expect(cat.name).to eq(certname)
320+
end
321+
322+
it 'returns the request response' do
323+
stub_request(:post, uri)
324+
.to_return(**catalog_response)
325+
326+
resp, _, _ = subject.post_catalog4(certname, payload)
327+
expect(resp).to be_a(Puppet::HTTP::Response)
328+
end
329+
330+
it 'raises a response error if unsuccessful' do
331+
stub_request(:post, uri)
332+
.to_return(status: [500, "Server Error"])
333+
334+
expect {
335+
subject.post_catalog4(certname, payload)
336+
}.to raise_error do |err|
337+
expect(err).to be_an_instance_of(Puppet::HTTP::ResponseError)
338+
expect(err.message).to eq('Server Error')
339+
expect(err.response.code).to eq(500)
340+
end
341+
end
342+
343+
it 'raises a response error when server response is not JSON' do
344+
stub_request(:post, uri)
345+
.to_return(body: "this isn't valid JSON", headers: {'Content-Type' => 'application/json'})
346+
347+
expect {
348+
subject.post_catalog4(certname, payload)
349+
}.to raise_error do |err|
350+
expect(err).to be_an_instance_of(Puppet::HTTP::SerializationError)
351+
expect(err.message).to match(/Failed to deserialize catalog from puppetserver response/)
352+
end
353+
end
354+
355+
it 'raises a response error when server response a JSON serialized catalog' do
356+
stub_request(:post, uri)
357+
.to_return(body: {oops: 'bad response data'}.to_json, headers: {'Content-Type' => 'application/json'})
358+
359+
expect {
360+
subject.post_catalog4(certname, payload)
361+
}.to raise_error do |err|
362+
expect(err).to be_an_instance_of(Puppet::HTTP::SerializationError)
363+
expect(err.message).to match(/Failed to deserialize catalog from puppetserver response/)
364+
end
365+
end
366+
367+
it 'raises ArgumentError when the `persistence` hash does not contain required keys' do
368+
payload[:persistence].delete(:facts)
369+
expect { subject.post_catalog4(certname, payload) }.to raise_error do |err|
370+
expect(err).to be_an_instance_of(ArgumentError)
371+
expect(err.message).to match(/The 'persistence' hash is missing the keys: facts/)
372+
end
373+
end
374+
375+
it 'raises ArgumentError when `facts` are not a Hash' do
376+
payload[:facts] = Puppet::Node::Facts.new(certname)
377+
expect { subject.post_catalog4(certname, payload) }.to raise_error do |err|
378+
expect(err).to be_an_instance_of(ArgumentError)
379+
expect(err.message).to match(/Facts must be a Hash not a Puppet::Node::Facts/)
380+
end
381+
end
382+
end
383+
261384
context 'when getting a node' do
262385
let(:uri) { %r{/puppet/v3/node/ziggy} }
263386
let(:node_response) { { body: formatter.render(node), headers: {'Content-Type' => formatter.mime } } }

0 commit comments

Comments
 (0)