Skip to content

Commit 5483382

Browse files
authored
Merge pull request #228 from seanmil/add_catalog_v4
Add Puppetserver catalog v4 API support
2 parents 8550721 + 94882db commit 5483382

19 files changed

+457
-15
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ There are some [limitations](doc/limitations.md) to a catalog-based approach, me
2323
`octocatalog-diff` is currently able to get catalogs by the following methods:
2424
- Compile catalog via the command line with a Puppet agent on your machine (as GitHub uses the tool internally)
2525
- Obtain catalog over the network from PuppetDB
26-
- Obtain catalog over the network using the API to query a Puppet Master / PuppetServer (Puppet 3.x and 4.x supported)
26+
- Obtain catalog over the network using the API to query a Puppet Master / PuppetServer (Puppet 3.x through 6.x supported)
2727
- Read catalog from a JSON file
2828

2929
## Example

doc/advanced-puppet-master.md

+23-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ Please note the following caveats:
88

99
0. You will need to deploy your Puppet code to an environment on your Puppet Master prior to running `octocatalog-diff` for that environment. `octocatalog-diff` does not deploy code for you.
1010

11-
0. You will need to configure authorization for one or more whitelisted certificates on your Puppet Master. The default permissions allow a node to retrieve its own catalog via the API, but you need a certificate for `octocatalog-diff` that permits it to retrieve any catalog. See the [Certificate authorization](#certificate-authorization) section below.
11+
0. You will need to configure authorization for one or more whitelisted certificates on your Puppet Master. The default permissions allow a node to retrieve its own catalog via the API, but you need a certificate for `octocatalog-diff` that permits it to retrieve any catalog. See the [Certificate authorization](#certificate-authorization) section below. If you are using Puppet Enterprise and use
12+
the Puppet Master v4 API you may also use a Puppet Enterprise RBAC token. The user owning the token will need the "Puppet Server Compile catalogs for remote nodes" permission.
13+
See the [PE RBAC Token Authorization](#pe-rbac-token-authorization) section below.
14+
15+
0. If you are using the v2 or v3 PuppetServer APIs with Octocatalog-Diff to compile catalogs, then those catalogs and facts will be automatically stored in PuppetDB. However, when using the v4 PuppetServer API with Octocatalog-Diff, facts and catalogs are *not* automatically stored in PuppetDB - persistence is optional and may be enabled with the appropriate Octocatalog-Diff CLI flag. If your environment depends on the accuracy of exported resources or facts in PuppetDB, you may wish to upgrade and use the V4 API, to avoid unintentional side-effects.
1216

1317
## Command line options
1418

@@ -18,11 +22,15 @@ The following command line options are used to retrieve a catalog from a Puppet
1822
| ------ | ----------- |
1923
| `-f ENVIRONMENT` | Environment name to use for the "from" catalog |
2024
| `-t ENVIRONMENT` | Environment name to use for the "to" catalog |
21-
| `--puppet-master HOSTNAME:PORT | The hostname and port number of the Puppet Master. (By default the port used by Puppet Master is 8140.) |
22-
| `--puppet-master-api-version VERSION | The API version used by the Puppet Master. API versions 2 and 3 are supported. Puppet Master 3.x uses API version 2, and the PuppetServer for Puppet 4.x uses API version 3. By default, API version 3 is used, so you only need to set this option if you are using Puppet Master 3.x. |
25+
| `--puppet-master HOSTNAME:PORT` | The hostname and port number of the Puppet Master. (By default the port used by Puppet Master is 8140.) |
26+
| `--puppet-master-api-version VERSION` | The API version used by the Puppet Master. API versions 2, 3,and 4 are supported. Puppet Master 3.x uses API version 2, and the PuppetServer for Puppet 4.x uses API version 3. PuppetServer 6.3.0 introduced the optional use of the v4 API but still fully supports the v3 API. By default, API version 3 is used, so you only need to set this option if you are using Puppet Master 3.x or wish to use the newer v4 API with PuppetServer 6. |
2327
| `--puppet-master-ssl-ca PATH` | Path to the CA certificate (public portion of certificate only) for your Puppet Master. This file will be on your Puppet Master and all Puppet agents. You can find it by running `puppet config print cacert` on any Puppet-managed host. |
24-
| `--puppet-master-ssl-client-cert PATH` | Path to the client certificate. Please see the section below on certificate authentication. |
25-
| `--puppet-master-ssl-client-key PATH` | Path to the client private key. Please see the section below on certificate authentication. |
28+
| `--puppet-master-ssl-client-cert PATH` | Path to the client certificate. Please see the section below on certificate authentication. This can be omitted if using PE RBAC token based auth with the v4 API. |
29+
| `--puppet-master-ssl-client-key PATH` | Path to the client private key. Please see the section below on certificate authentication. This can be omitted if using PE RBAC token based auth with the v4 API. |
30+
| `--puppet-master-token STRING` | A PE RBAC token used to authenticate a v4 catalog compile, in lieu of using certificate authentication. Please see the section below on token authentication. |
31+
| `--puppet-master-token-file PATH` | A path to a file containing a PE RBAC token used to authenticate a v4 catalog compile, in lieu of using certificate authentication. If this and `--puppet-master-token` are both specified, `--puppet-master-token` will be used instead. Please see the section below on token authentication. |
32+
| `--puppet-master-update-catalog` | When using the v4 API, instruct the PuppetServer to update the catalog generated from the compile in its PuppetDB instance. When using v2 and v3 APIs the catalog is always updated and this option is ignored. |
33+
| `--puppet-master-update-facts` | When using the v4 API, instruct the PuppetServer to update the facts used during the compile in its PuppetDB instance. When using v2 and v3 APIs the facts are always updated and this option is ignored. |
2634

2735
If you wish to use a different Puppet Master to compile the "to" and "from" catalogs, you may prefix any of the `--puppet-master...` options with `to` or `from`. For example, perhaps you are testing an upgrade from Puppet 3.x to 4.x. You could use:
2836

@@ -48,3 +56,13 @@ allow $1
4856
```
4957

5058
Please follow the instructions for the version of Puppet Master, PuppetServer, or Puppet Enterprise that you are using in order to generate and authorize the certificates.
59+
60+
## PE RBAC Token authorization
61+
62+
In newer versions of Puppet Enterprise you can authenticate using a valid PE RBAC token with appropriate permissions as long as it is authorized in the PuppetServer `auth.conf` file.
63+
64+
By default this permission is enabled and controlled by the `puppet_enterprise::master::tk_authz::allow_rbac_catalog_compile` Hiera setting.
65+
66+
The user the token was issued to must have the `puppetserver:compile_catalogs:*` permission.
67+
68+
Note: A Puppet catalog may contain unencrypted secrets, even ones marked as `Sensitive`. In order to perform its job, Octocatalog-Diff needs access to the catalog. By granting a user the above RBAC permission you are granting them the ability to retrieve and view the complete catalog resulting from a compile, including any included secrets.

lib/octocatalog-diff/catalog.rb

+2
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ def resources
191191
build
192192
raise OctocatalogDiff::Errors::CatalogError, 'Catalog does not appear to have been built' if !valid? && error_message.nil?
193193
raise OctocatalogDiff::Errors::CatalogError, error_message unless valid?
194+
# Handle the structure returned by the /puppet/v4/catalog Puppetserver endpoint:
195+
return @catalog['catalog']['resources'] if @catalog['catalog'].is_a?(Hash) && @catalog['catalog']['resources'].is_a?(Array)
194196
return @catalog['data']['resources'] if @catalog['data'].is_a?(Hash) && @catalog['data']['resources'].is_a?(Array)
195197
return @catalog['resources'] if @catalog['resources'].is_a?(Array)
196198
# This is a bug condition

lib/octocatalog-diff/catalog/puppetmaster.rb

+43-5
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,19 @@ def build_catalog(logger = Logger.new(StringIO.new))
6262
fetch_catalog(logger)
6363
end
6464

65-
# Returns a hash of parameters for each supported version of the Puppet Server Catalog API.
65+
# Returns a hash of parameters for the requested version of the Puppet Server Catalog API.
6666
# @return [Hash] Hash of parameters
6767
#
6868
# Note: The double escaping of the facts here is implemented to correspond to a long standing
6969
# bug in the Puppet code. See https://github.com/puppetlabs/puppet/pull/1818 and
7070
# https://docs.puppet.com/puppet/latest/http_api/http_catalog.html#parameters for explanation.
71-
def puppet_catalog_api
72-
{
71+
def puppet_catalog_api(version)
72+
api_style = {
7373
2 => {
7474
url: "https://#{@options[:puppet_master]}/#{@options[:branch]}/catalog/#{@node}",
75+
headers: {
76+
'Accept' => 'text/pson'
77+
},
7578
parameters: {
7679
'facts_format' => 'pson',
7780
'facts' => CGI.escape(@facts.fudge_timestamp.without('trusted').to_pson),
@@ -80,24 +83,59 @@ def puppet_catalog_api
8083
},
8184
3 => {
8285
url: "https://#{@options[:puppet_master]}/puppet/v3/catalog/#{@node}",
86+
headers: {
87+
'Accept' => 'text/pson'
88+
},
8389
parameters: {
8490
'environment' => @options[:branch],
8591
'facts_format' => 'pson',
8692
'facts' => CGI.escape(@facts.fudge_timestamp.without('trusted').to_pson),
8793
'transaction_uuid' => SecureRandom.uuid
8894
}
95+
},
96+
4 => {
97+
url: "https://#{@options[:puppet_master]}/puppet/v4/catalog",
98+
headers: {
99+
'Content-Type' => 'application/json'
100+
},
101+
parameters: {
102+
'certname' => @node,
103+
'persistence' => {
104+
'facts' => @options[:puppet_master_update_facts] || false,
105+
'catalog' => @options[:puppet_master_update_catalog] || false
106+
},
107+
'environment' => @options[:branch],
108+
'facts' => { 'values' => @facts.facts['values'] },
109+
'options' => {
110+
'prefer_requested_environment' => true,
111+
'capture_logs' => false,
112+
'log_level' => 'warning'
113+
},
114+
'transaction_uuid' => SecureRandom.uuid
115+
}
89116
}
90117
}
118+
119+
params = api_style[version]
120+
return nil if params.nil?
121+
122+
unless @options[:puppet_master_token].nil?
123+
params[:headers]['X-Authentication'] = @options[:puppet_master_token]
124+
end
125+
126+
params[:parameters] = params[:parameters].to_json if version >= 4
127+
128+
params
91129
end
92130

93131
# Fetch catalog by contacting the Puppet master, sending the facts, and asking for the catalog. When the
94132
# catalog is returned in PSON format, parse it to JSON and then set appropriate variables.
95133
def fetch_catalog(logger)
96134
api_version = @options[:puppet_master_api_version] || DEFAULT_PUPPET_SERVER_API
97-
api = puppet_catalog_api[api_version]
135+
api = puppet_catalog_api(api_version)
98136
raise ArgumentError, "Unsupported or invalid API version #{api_version}" unless api.is_a?(Hash)
99137

100-
more_options = { headers: { 'Accept' => 'text/pson' }, timeout: @timeout }
138+
more_options = { headers: api[:headers], timeout: @timeout }
101139
post_hash = api[:parameters]
102140

103141
response = nil

lib/octocatalog-diff/cli/options.rb

+35
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def self.option_globally_or_per_branch(opts = {})
103103
datatype = opts.fetch(:datatype, '')
104104
return option_globally_or_per_branch_string(opts) if datatype.is_a?(String)
105105
return option_globally_or_per_branch_array(opts) if datatype.is_a?(Array)
106+
return option_globally_or_per_branch_boolean(opts) if datatype.is_a?(TrueClass) || datatype.is_a?(FalseClass)
106107
raise ArgumentError, "option_globally_or_per_branch not equipped to handle #{datatype.class}"
107108
end
108109

@@ -177,6 +178,40 @@ def self.option_globally_or_per_branch_array(opts = {})
177178
end
178179
end
179180

181+
# See description of `option_globally_or_per_branch`. This implements the logic for a boolean value.
182+
# @param :parser [OptionParser object] The OptionParser argument
183+
# @param :options [Hash] Options hash being constructed; this is modified in this method.
184+
# @param :cli_name [String] Name of option on command line (e.g. puppet-binary)
185+
# @param :option_name [Symbol] Name of option in the options hash (e.g. :puppet_binary)
186+
# @param :desc [String] Description of option on the command line; will have "for the XX branch" appended
187+
def self.option_globally_or_per_branch_boolean(opts)
188+
parser = opts.fetch(:parser)
189+
options = opts.fetch(:options)
190+
cli_name = opts.fetch(:cli_name)
191+
option_name = opts.fetch(:option_name)
192+
desc = opts.fetch(:desc)
193+
194+
flag = cli_name
195+
from_option = "from_#{option_name}".to_sym
196+
to_option = "to_#{option_name}".to_sym
197+
parser.on("--[no-]#{flag}", "#{desc} globally") do |x|
198+
translated = translate_option(opts[:translator], x)
199+
options[to_option] = translated
200+
options[from_option] = translated
201+
post_process(opts[:post_process], options)
202+
end
203+
parser.on("--[no-]to-#{flag}", "#{desc} for the to branch") do |x|
204+
translated = translate_option(opts[:translator], x)
205+
options[to_option] = translated
206+
post_process(opts[:post_process], options)
207+
end
208+
parser.on("--[no-]from-#{flag}", "#{desc} for the from branch") do |x|
209+
translated = translate_option(opts[:translator], x)
210+
options[from_option] = translated
211+
post_process(opts[:post_process], options)
212+
end
213+
end
214+
180215
# If a validator was provided, run the validator on the supplied value. The validator is expected to
181216
# throw an error if there is a problem. Note that the validator runs *before* the translator if both
182217
# a validator and translator are supplied.

lib/octocatalog-diff/cli/options/puppet_master_api_version.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def parse(parser, options)
1414
options: options,
1515
cli_name: 'puppet-master-api-version',
1616
option_name: 'puppet_master_api_version',
17-
desc: 'Puppet Master API version (2 for Puppet 3.x, 3 for Puppet 4.x)',
18-
validator: ->(x) { x =~ /^[23]$/ || raise(ArgumentError, 'Only API versions 2 and 3 are supported') },
17+
desc: 'Puppet Master API version (2 for Puppet 3.x, 3 for Puppet 4.x, 4 for Puppet Server >= 6.3.0)',
18+
validator: ->(x) { x =~ /^[234]$/ || raise(ArgumentError, 'Only API versions 2, 3, and 4 are supported') },
1919
translator: ->(x) { x.to_i }
2020
)
2121
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
# Specify a PE RBAC token used to authenticate to Puppetserver for v4
4+
# catalog API calls.
5+
# @param parser [OptionParser object] The OptionParser argument
6+
# @param options [Hash] Options hash being constructed; this is modified in this method.
7+
OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_token) do
8+
has_weight 310
9+
10+
def parse(parser, options)
11+
OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12+
parser: parser,
13+
options: options,
14+
datatype: '',
15+
cli_name: 'puppet-master-token',
16+
option_name: 'puppet_master_token',
17+
desc: 'PE RBAC token to authenticate to the Puppetserver API v4'
18+
)
19+
end
20+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# Specify a path to a file containing a PE RBAC token used to authenticate to the
4+
# Puppetserver for a v4 catalog API call.
5+
# @param parser [OptionParser object] The OptionParser argument
6+
# @param options [Hash] Options hash being constructed; this is modified in this method.
7+
OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_token_file) do
8+
has_weight 300
9+
10+
def parse(parser, options)
11+
OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12+
parser: parser,
13+
options: options,
14+
datatype: '',
15+
cli_name: 'puppet-master-token-file',
16+
option_name: 'puppet_master_token_file',
17+
desc: 'File containing PE RBAC token to authenticate to the Puppetserver API v4',
18+
translator: ->(x) { x.start_with?('/', '~') ? x : File.join(options[:basedir], x) },
19+
post_process: lambda do |opts|
20+
%w(to from).each do |prefix|
21+
fileopt = "#{prefix}_puppet_master_token_file".to_sym
22+
tokenopt = "#{prefix}_puppet_master_token".to_sym
23+
24+
tokenfile = opts[fileopt]
25+
next if tokenfile.nil?
26+
27+
raise(Errno::ENOENT, "Token file #{tokenfile} is not readable") unless File.readable?(tokenfile)
28+
29+
token = File.read(tokenfile).strip
30+
opts[tokenopt] ||= token
31+
end
32+
end
33+
)
34+
end
35+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
# Specify if, when using the Puppetserver v4 catalog API, the Puppetserver should
4+
# update the catalog in PuppetDB.
5+
# @param parser [OptionParser object] The OptionParser argument
6+
# @param options [Hash] Options hash being constructed; this is modified in this method.
7+
OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_update_catalog) do
8+
has_weight 320
9+
10+
def parse(parser, options)
11+
OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12+
parser: parser,
13+
options: options,
14+
datatype: false,
15+
cli_name: 'puppet-master-update-catalog',
16+
option_name: 'puppet_master_update_catalog',
17+
desc: 'Update catalog in PuppetDB when using Puppetmaster API version 4'
18+
)
19+
end
20+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
# Specify if, when using the Puppetserver v4 catalog API, the Puppetserver should
4+
# update the facts in PuppetDB.
5+
# @param parser [OptionParser object] The OptionParser argument
6+
# @param options [Hash] Options hash being constructed; this is modified in this method.
7+
OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_update_facts) do
8+
has_weight 320
9+
10+
def parse(parser, options)
11+
OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12+
parser: parser,
13+
options: options,
14+
datatype: false,
15+
cli_name: 'puppet-master-update-facts',
16+
option_name: 'puppet_master_update_facts',
17+
desc: 'Update facts in PuppetDB when using Puppetmaster API version 4'
18+
)
19+
end
20+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"catalog": {
3+
"tags": ["settings"],
4+
"name": "my.rspec.node",
5+
"version": "production",
6+
"code_id": null,
7+
"catalog_uuid": "89869359-db50-472f-b435-1d37c22be9eb",
8+
"catalog_format": 1,
9+
"environment": "production",
10+
"resources": [
11+
{
12+
"type": "Stage",
13+
"title": "main",
14+
"tags": ["stage"],
15+
"exported": false,
16+
"parameters": {
17+
"name": "main"
18+
}
19+
},
20+
{
21+
"type": "Class",
22+
"title": "Settings",
23+
"tags": ["class","settings"],
24+
"exported": false
25+
}
26+
],
27+
"edges": [
28+
{
29+
"source": "Stage[main]",
30+
"target": "Class[Settings]"
31+
},
32+
{
33+
"source": "Stage[main]",
34+
"target": "Class[main]"
35+
}
36+
],
37+
"classes": [
38+
"settings"
39+
]
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secretpuppetmastertoken

0 commit comments

Comments
 (0)