diff --git a/lib/pe_client/client.rb b/lib/pe_client/client.rb index 47af299..14a7c1b 100644 --- a/lib/pe_client/client.rb +++ b/lib/pe_client/client.rb @@ -214,6 +214,12 @@ def puppet_ca_v1 @puppet_ca_v1 ||= Resource::PuppetCAV1.new(self) end + # @return [Resource::PuppetDB] + def puppet_db + require_relative "resources/puppet_db" + @puppet_db ||= Resource::PuppetDB.new(self) + end + private # Handle HTTP response diff --git a/lib/pe_client/resources/puppet_db.rb b/lib/pe_client/resources/puppet_db.rb new file mode 100644 index 0000000..8d6add2 --- /dev/null +++ b/lib/pe_client/resources/puppet_db.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Copyright 2025 Perforce Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "base_with_port" + +module PEClient + module Resource + # Interact with PuppetDB + # + # @see https://help.puppet.com/pdb/current/topics/api.htm + class PuppetDB < BaseWithPort + # The base path for PuppetDB endpoints. + BASE_PATH = "/pdb" + + # Default PuppetDB API Port + PORT = 8080 + + # @return [PuppetDB::QueryV4] + def query_v4 + require_relative "puppet_db/query.v4" + @query_v4 ||= PuppetDB::QueryV4.new(@client) + end + end + end +end diff --git a/lib/pe_client/resources/puppet_db/query.v4.rb b/lib/pe_client/resources/puppet_db/query.v4.rb new file mode 100644 index 0000000..3377ca8 --- /dev/null +++ b/lib/pe_client/resources/puppet_db/query.v4.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true + +# Copyright 2025 Perforce Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "../base" + +module PEClient + module Resource + class PuppetDB + # PuppetDB's query API can retrieve data objects from PuppetDB for use in other applications. + # + # @see https://help.puppet.com/pdb/current/topics/api_query.htm + class QueryV4 < Base + # The base path for PuppetDB Query v4 endpoints. + BASE_PATH = "#{PuppetDB::BASE_PATH}/query/v4".freeze + + # @!macro query + # @param query [Array] An Array of query predicates, in prefix notation (["", "", ""]). + + # @!macro query_paging + # Most of PuppetDB's query endpoints support a general set of HTTP URL parameters that can be used for paging results. + # PuppetDB also supports paging via query operators, as described in the AST documentation. + # + # @param kwargs [Hash] Keyword arguments for paging + # @option kwargs [String] :order_by This parameter can be used to ask PuppetDB to return results sorted by one or more fields, in ascending or descending order. + # The value must be an Array of Hashes. + # Each map represents a field to sort by, and the order in which the maps are specified in the array determines the sort order. + # Each map must contain the key field, whose value must be the name of a field that can be returned by the specified query. + # Each map may also optionally contain the key order, whose value may either be "asc" or "desc", depending on whether you wish the field to be sorted in ascending or descending order. + # The default value for this key, if not specified, is "asc". + # Note that the legal values for field vary depending on which endpoint you are querying. + # For lists of legal fields, please refer to the documentation for the specific query endpoints. + # @option kwargs [Integer] :limit This parameter can be used to restrict the result set to a maximum number of results. + # @option kwargs [Boolean] :include_total This parameter lets you request a count of how many records would have been returned, had the query not been limited using the limit parameter. + # This is useful if you want your application to show how far the user has navigated ("page 3 of 15"). + # The value should be a Boolean, and defaults to `false`. + # If `true`, the HTTP response will contain a header X-Records, whose value is an integer indicating the total number of results available. + # Note: Setting this flag to `true` can decrease performance. + # @option kwargs [Integer] :offset This parameter can be used to tell PuppetDB to return results beginning at the specified offset. + # For example, if you'd like to page through query results with a page size of 10, your first query would specify `limit: 10` and `offset: 0`, your second query would specify `limit: 10` and `offset: 10`, and so on. + # Note that the order in which results are returned by PuppetDB is not guaranteed to be consistent unless you specify a value for `:order_by`, so this parameter should generally be used in conjunction with `:order_by`. + # + # @return [Hash] + + # The root query endpoint can be used to retrieve any known entities from a single endpoint. + # + # @param query [String,Array] Either a PQL query string, or an AST JSON array containing the query in prefix notation (["from", "", ["", "", ""]]). + # Unlike other endpoints, a query with a from is required to choose the entity for which to query. For general info about queries, see our guide to query structure. + # @param timeout [Integer] An optional limit on the number of seconds that the query will be allowed to run (e.g. timeout=30). + # If the limit is reached, the query will be interrupted. + # At the moment, that will result in either a 500 HTTP response status, or (more likely) a truncated JSON result if the result has begun streaming. + # Specifying this parameter is strongly encouraged. + # Lingering queries can consume substantial server resources (particularly on the PostgreSQL server) decreasing performance, for example, and increasing the maximum required storage space. + # The query timeout configuration settings are also recommended. + # @param ast_only [Boolean] When true, the query response will be the supplied query in AST, either exactly as supplied or translated from PQL. + # `False` by default. + # @param origin [String] A string describing the source of the query. + # It can be anything, and will be reported in the log when PuppetDB is configured to log queries. + # Note that Puppet intends to use origin names beginning with puppet: for its own queries, so it is recommended that other clients choose something else. + # @param explain [String] The string value "analyze". + # This parameter can be used to tell PuppetDB to return the execution plan of a statement instead of the query results. + # The execution plan shows how the table(s) referenced by the statement will be scanned, the estimated statement execution cost and the actual run time statistics. + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/root_endpoint.htm + def root(query:, timeout: nil, ast_only: nil, origin: nil, explain: nil, **kwargs) + @client.get BASE_PATH, params: {query: query.to_json, timeout:, ast_only:, origin:, explain:}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # Environments are semi-isolated groups of nodes managed by Puppet. + # Nodes are assigned to environments by their own configuration, or by the Puppet Server's external node classifier. + # When PuppetDB collects info about a node, it keeps track of the environment the node is assigned to. + # PuppetDB also keeps a list of environments it has seen. + # You can query this list by making an HTTP request to the environments endpoint. + # + # @param environment [String] This will return the name of the environment if it currently exists in PuppetDB. + # @param type [String] "events", "facts", "reports", or "resources" + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/environments.htm + def environments(environment: nil, type: nil, query: nil, **kwargs) + uri = "#{BASE_PATH}/environments" + uri += "/#{environment}" if environment + uri += "/#{type}" if environment && type + @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # Producers are the Puppet Servers that send reports, catalogs, and factsets to PuppetDB. + # When PuppetDB stores a report, catalog, or factset, it keeps track of the producer of the report/catalog/factset. + # PuppetDB also keeps a list of producers it has seen. + # You can query this list by making an HTTP request to the producers endpoint. + # + # @param producer [String] This will return the name of the producer if it currently exists in PuppetDB. + # @param type [String] "catalogs", "factsets", or "reports" + # @macro query + # @macro query_paging + # + # @return [Array, Hash] + # + # @see https://help.puppet.com/pdb/current/topics/producers.htm + def producers(producer: nil, type: nil, query: nil, **kwargs) + uri = "#{BASE_PATH}/producers" + uri += "/#{producer}" if producer + uri += "/#{type}" if producer && type + @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The facts endpoint provides access to a representation of node factsets where a result is returned for each top-level key in the node's structured factset. + # Note that the {#inventory} endpoint will often provide more flexible and efficient access to the same information. + # + # @param fact_name [String] This will return all facts with the given fact name, for all nodes. + # @param value [String] This will return all facts with the given fact name and value, for all nodes. + # (That is, only the certname field will differ in each result.) + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/facts.htm + def facts(fact_name: nil, value: nil, query: nil, **kwargs) + uri = "#{BASE_PATH}/facts" + uri += "/#{fact_name}" if fact_name + uri += "/#{value}" if fact_name && value + @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The fact_names endpoint can be used to retrieve all known fact names. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/fact-names.htm + def fact_names(query: nil, **kwargs) + @client.get "#{BASE_PATH}/fact-names", params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The fact_paths endpoint retrieves the set of all known fact paths for all known nodes, and is intended as a counterpart to the {#fact_names} endpoint, providing increased granularity around structured facts. + # The endpoint may be useful for building autocompletion in GUIs or for other applications that require a basic top-level view of fact paths. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/fact-paths.htm + def fact_paths(query: nil, **kwargs) + @client.get "#{BASE_PATH}/fact-paths", params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The fact_contents endpoint provides selective access to factset subtrees via fact paths. + # Note that the {#inventory} endpoint will often provide more flexible and efficient access to the same information. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/fact-contents.htm + def fact_contents(query: nil, **kwargs) + @client.get "#{BASE_PATH}/fact-contents", params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The inventory endpoint provides an alternate and potentially more efficient way to access structured facts as compared to the {#facts}, {#fact_contents}, and {#factsets} endpoints. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/inventory.htm + def inventory(query: nil, **kwargs) + @client.get "#{BASE_PATH}/inventory", params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # You can query resources by making an HTTP request to the resources endpoint. + # + # @param type [String] This will return all resources for all nodes with the given type. + # @param title [String] This will return all resources for all nodes with the given type and title. + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/resources.htm + def resources(type: nil, title: nil, query: nil, **kwargs) + uri = "#{BASE_PATH}/resources" + uri += "/#{type}" if type + uri += "/#{URI.encode_www_form_component(title)}" if type && title + @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # Catalog edges are relationships formed between two resources. + # They represent the edges inside the catalog graph, whereas resources represent the nodes in the graph. + # You can query edges by making a request to the edges endpoint. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/edges.htm + def edges(query: nil, **kwargs) + @client.get "#{BASE_PATH}/edges", params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # Puppet agent nodes submit reports after their runs, and the Puppet Server forwards these to PuppetDB. Each report includes: + # - Data about the entire run + # - Metadata about the report + # - Many events, describing what happened during the run + # + # After this information is stored in PuppetDB, it can be queried in various ways. + # - You can query data about the run and report metadata by making an HTTP request to the {#reports} endpoint. + # - You can query data about individual events by making an HTTP request to the {#events} endpoint. + # - You can query summaries of event data by making an HTTP request to the {#event_counts} or {#aggregate_event_counts} endpoints. + # + # @param distinct_resources [Boolean] If specified, the result set will only return the most recent event for a given resource on a given node. + # (EXPERIMENTAL: it is possible that the behavior of this parameter may change in future releases.) + # @param distinct_start_time [String] An ISO 8601 timestamp. + # Required if `distinct_resources` is `true`. + # @param distinct_end_time [String] An ISO 8601 timestamp. + # Required if `distinct_resources` is `true`. + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/events.htm + def events(distinct_resources: nil, distinct_start_time: nil, distinct_end_time: nil, query: nil, **kwargs) + @client.get "#{BASE_PATH}/events", + params: {distinct_resources:, distinct_start_time:, distinct_end_time:, query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The event_counts endpoint is designated as experimental. It may be altered or removed in a future release. + # Puppet agent nodes submit reports after their runs, and the Puppet Server forwards these to PuppetDB. Each report includes: + # - Data about the entire run + # - Metadata about the report + # - Many events, describing what happened during the run + # + # After this information is stored in PuppetDB, it can be queried in various ways. + # - You can query data about the run and report metadata by making an HTTP request to the {#reports} endpoint. + # - You can query data about individual events by making an HTTP request to the {#events} endpoint. + # - You can query summaries of event data by making an HTTP request to the {#event_counts} or {#aggregate_event_counts} endpoints. + # + # @param summarize_by [String] A string specifying which type of object you'd like to see counts for. + # Supported values are "resource", "containing_class", and "certname". + # @param count_by [String] A string specifying what type of object is counted when building up the counts of successes, failures, noops, and skips. + # Supported values are "resource" (default) and "certname". + # @param counts_filter [Array] An Array of query predicates, in prefix notation (["", "", ""]). + # @param distinct_resources [Boolean] This parameter is passed along to the {#events} endpoint. + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/event-counts.htm + # @api experimental + def event_counts(summarize_by:, count_by: nil, counts_filter: nil, distinct_resources: nil, query: nil, **kwargs) + @client.get "#{BASE_PATH}/event-counts", + params: {summarize_by:, count_by:, counts_filter: counts_filter&.to_json, distinct_resources:, query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # The aggregate_event_counts endpoint is designated as experimental. It may be altered or removed in a future release. + # Puppet agent nodes submit reports after their runs, and the Puppet Server forwards these to PuppetDB. Each report includes: + # - Data about the entire run. + # - Metadata about the report. + # - Many events, describing what happened during the run. + # + # After this information is stored in PuppetDB, it can be queried in various ways. + # - You can query data about the run and report metadata by making an HTTP request to the {#reports} endpoint. + # - You can query data about individual events by making an HTTP request to the {#events} endpoint. + # - You can query summaries of event data by making an HTTP request to the {#event_counts} or {#aggregate_event_counts} endpoints. + # + # @param summarize_by [String] A string specifying which type of object you'd like to see counts for. + # Supported values are "resource", "containing_class", and "certname". + # @param count_by [String] A string specifying what type of object is counted when building up the counts of successes, failures, noops, and skips. + # Supported values are "resource" (default) and "certname". + # @param counts_filter [Array] An Array of query predicates, in prefix notation (["", "", ""]). + # @param distinct_resources [Boolean] This parameter is passed along to the {#events} endpoint. + # @macro query + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/aggregate-event-counts.htm + # @api experimental + def aggregate_event_counts(summarize_by:, count_by: nil, counts_filter: nil, distinct_resources: nil, query: nil) + @client.get "#{BASE_PATH}/aggregate-event-counts", + params: {summarize_by:, count_by:, counts_filter: counts_filter&.to_json, distinct_resources:, query: query&.to_json}.compact + end + + # Returns all installed packages, across all nodes. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/packages.htm#pdbqueryv4packages + def packages(query: nil, **kwargs) + @client.get "#{BASE_PATH}/packages", params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # Returns all installed packages along with the certname of the nodes they are installed on or a specific node. + # + # @param certname [String] + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/packages.htm#pdbqueryv4package-inventory + # @see https://help.puppet.com/pdb/current/topics/packages.htm#pdbqueryv4package-inventorycertname + def package_inventory(certname: nil, query: nil, **kwargs) + @client.get certname ? "#{BASE_PATH}/package-inventory/#{certname}" : "#{BASE_PATH}/package-inventory", + params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # @return [QueryV4::Nodes] + def nodes + require_relative "query.v4/nodes" + @nodes ||= QueryV4::Nodes.new(@client) + end + + # @return [QueryV4::Factsets] + def factsets + require_relative "query.v4/factsets" + @factsets ||= QueryV4::Factsets.new(@client) + end + + # @return [QueryV4::Catalogs] + def catalogs + require_relative "query.v4/catalogs" + @catalogs ||= QueryV4::Catalogs.new(@client) + end + + # @return [QueryV4::Reports] + def reports + require_relative "query.v4/reports" + @reports ||= QueryV4::Reports.new(@client) + end + + # @macro query_paging + # @api private + def self.query_paging(**kwargs) + { + order_by: kwargs[:order_by], + limit: kwargs[:limit], + include_total: kwargs[:include_total], + offset: kwargs[:offset] + } + end + end + end + end +end diff --git a/lib/pe_client/resources/puppet_db/query.v4/catalogs.rb b/lib/pe_client/resources/puppet_db/query.v4/catalogs.rb new file mode 100644 index 0000000..37ff640 --- /dev/null +++ b/lib/pe_client/resources/puppet_db/query.v4/catalogs.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Copyright 2025 Perforce Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "../../base" + +module PEClient + module Resource + class PuppetDB + class QueryV4 + # You can query catalogs by making an HTTP request to the catalogs endpoint. + # + # @see https://help.puppet.com/pdb/current/topics/catalogs.htm + class Catalogs < Base + # The base path for PuppetDB Query v4 Catalogs endpoints. + BASE_PATH = "#{QueryV4::BASE_PATH}/catalogs".freeze + + # This will return a JSON array containing the most recent catalog for each node or for a given node in your infrastructure. + # + # @param node [String] + # @macro query + # @macro query_paging + # + # @return [Array, Hash] + # + # @see https://help.puppet.com/pdb/current/topics/catalogs.htm#pdbqueryv4catalogs + # @see https://help.puppet.com/pdb/current/topics/catalogs.htm#pdbqueryv4catalogsnode + def get(node: nil, query: nil, **kwargs) + @client.get node ? "#{BASE_PATH}/#{node}" : BASE_PATH, + params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # This will return all edges for a particular catalog, designated by a node certname. + # This is a shortcut to the {QueryV4#edges} endpoint. It behaves the same as a call to {QueryV4#edges} with `query: ["=", "certname", ""]`. + # Except results are returned even if the node is deactivated or expired. + # + # @param node [String] + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/catalogs.htm#pdbqueryv4catalogsnodeedges + def edges(node:, **kwargs) + @client.get "#{BASE_PATH}/#{node}/edges", params: QueryV4.query_paging(**kwargs).compact + end + + # This will return all resources for a particular catalog, designated by a node certname. + # This is a shortcut to the {QueryV4#resources} endpoint. It behaves the same as a call to {QueryV4#resources} with `query: ["=", "certname", ""]`. + # Except results are returned even if the node is deactivated or expired. + # + # @param node [String] + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/catalogs.htm#pdbqueryv4catalogsnoderesources + def resources(node:, **kwargs) + @client.get "#{BASE_PATH}/#{node}/resources", params: QueryV4.query_paging(**kwargs).compact + end + end + end + end + end +end diff --git a/lib/pe_client/resources/puppet_db/query.v4/factsets.rb b/lib/pe_client/resources/puppet_db/query.v4/factsets.rb new file mode 100644 index 0000000..ba7d7d2 --- /dev/null +++ b/lib/pe_client/resources/puppet_db/query.v4/factsets.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Copyright 2025 Perforce Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "../../base" + +module PEClient + module Resource + class PuppetDB + class QueryV4 + # The factsets endpoint provides access to a representation of node factsets where each result includes the structured facts for a node broken down into a vector of top-level key/value pairs. + # Note that the inventory endpoint will often provide more flexible and efficient access to the same information. + # + # @see https://help.puppet.com/pdb/current/topics/factsets.htm + class Factsets < Base + # The base path for PuppetDB Query v4 Factsets endpoints. + BASE_PATH = "#{QueryV4::BASE_PATH}/factsets".freeze + + # This will return all factsets matching the given query. + # + # @param node [String] This will return the most recent factset for the given node. + # @macro query + # @macro query_paging + # + # @return [Array, Hash] + # + # @see {QueryV4#query_paging} for paging options + # @see https://help.puppet.com/pdb/current/topics/factsets.htm#pdbqueryv4factsets + # @see https://help.puppet.com/pdb/current/topics/factsets.htm#pdbqueryv4factsetsnode + def get(node: nil, query: nil, **kwargs) + @client.get node ? "#{BASE_PATH}/#{node}" : BASE_PATH, + params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # This will return all facts for a particular factset, designated by a node certname. + # This is a shortcut to the {QueryV4#facts} endpoint. + # It behaves the same as a call to {QueryV4#facts} with a query string of ["=", "certname", ""], except results are returned even if the node is deactivated or expired. + # + # @param node [String] + # @macro query_paging + # + # @return [Array] + # + # @see {QueryV4#facts} for more details + # @see https://help.puppet.com/pdb/current/topics/factsets.htm#pdbqueryv4factsetsnodefacts + def facts(node:, **kwargs) + @client.get "#{BASE_PATH}/#{node}/facts", params: QueryV4.query_paging(**kwargs).compact + end + end + end + end + end +end diff --git a/lib/pe_client/resources/puppet_db/query.v4/nodes.rb b/lib/pe_client/resources/puppet_db/query.v4/nodes.rb new file mode 100644 index 0000000..8985021 --- /dev/null +++ b/lib/pe_client/resources/puppet_db/query.v4/nodes.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# Copyright 2025 Perforce Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "../../base" + +module PEClient + module Resource + class PuppetDB + class QueryV4 + # Nodes can be queried by making requests through these endpoints. + # + # @see https://help.puppet.com/pdb/current/topics/nodes.htm + class Nodes < Base + # The base path for PuppetDB Query v4 Nodes endpoints. + BASE_PATH = "#{QueryV4::BASE_PATH}/nodes".freeze + + # This will return all nodes matching the given query. + # Deactivated and expired nodes aren't included in the response. + # + # @param node [String] This will return status information for the given node, active or not. + # It behaves exactly like a call with `node: nil` but with a query string of ["=", "certname", ""]. + # @macro query + # @macro query_paging + # + # @return [Array, Hash] + # + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodes + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnode + def get(node: nil, query: nil, **kwargs) + @client.get node ? "#{BASE_PATH}/#{node}" : BASE_PATH, + params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # This will return the facts for the given node. Facts from deactivated and expired nodes aren't included in the response. + # This is a shortcut to the {QueryV4#facts} endpoint. + # It behaves the same as a call to {QueryV4#facts} with a query string of ["=", "certname", ""]. + # Facts from deactivated and expired nodes aren't included in the response. + # + # @param node [String] + # @param name [String] This will return facts with the given name for the given node. + # @param value [String] This will return facts with the given name and value for the given node. + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnodefacts + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnodefactsname + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnodefactsnamevalue + def facts(node:, name: nil, value: nil, query: nil, **kwargs) + uri = "#{BASE_PATH}/#{node}/facts" + uri += "/#{name}" if name + uri += "/#{value}" if name && value + @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # This will return the resources for the given node. + # Resources from deactivated and expired nodes aren't included in the response. + # This is a shortcut to the {QueryV4#resources} endpoint. + # It behaves the same as a call to {QueryV4#resources} with a query string of ["=", "certname", ""]. + # + # @param node [String] + # @param type [String] This will return the resources of the indicated type for the given node. + # @param title [String] This will return the resource of the indicated type and title for the given node. + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnoderesources + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnoderesourcestype + # @see https://help.puppet.com/pdb/current/topics/nodes.htm#pdbqueryv4nodesnoderesourcestypetitle + def resources(node:, type: nil, title: nil, query: nil, **kwargs) + uri = "#{BASE_PATH}/#{node}/resources" + uri += "/#{type}" if type + uri += "/#{title}" if type && title + @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + end + end + end + end +end diff --git a/lib/pe_client/resources/puppet_db/query.v4/reports.rb b/lib/pe_client/resources/puppet_db/query.v4/reports.rb new file mode 100644 index 0000000..538d673 --- /dev/null +++ b/lib/pe_client/resources/puppet_db/query.v4/reports.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# Copyright 2025 Perforce Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "../../base" + +module PEClient + module Resource + class PuppetDB + class QueryV4 + # Puppet agent nodes submit reports after their runs, and the Puppet Server forwards these to PuppetDB. Each report includes: + # - Data about the entire run + # - Metadata about the report + # - Many events, describing what happened during the run + # + # After this information is stored in PuppetDB, it can be queried in various ways. + # - You can query data about the run and report metadata by making an HTTP request to the reports endpoint. + # - You can query data about individual events by making an HTTP request to the {QueryV4#events} endpoint. + # - You can query summaries of event data by making an HTTP request to the {QueryV4#event_counts} or {QueryV4#aggregate_event_counts} endpoints. + # + # @see https://help.puppet.com/pdb/current/topics/reports.htm + class Reports < Base + # The base path for PuppetDB Query v4 Reports endpoints. + BASE_PATH = "#{QueryV4::BASE_PATH}/reports".freeze + + # If `:query` is absent, PuppetDB will return all reports. + # + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/reports.htm#pdbqueryv4reports + def get(query: nil, **kwargs) + @client.get(BASE_PATH, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact) + end + + # Returns all events for a particular report, designated by its unique hash. + # + # @param hash [String] The unique hash of the report. + # @macro query + # @macro query_paging + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/reports.htm#pdbqueryv4reportshashevents + def events(hash:, query: nil, **kwargs) + @client.get "#{BASE_PATH}/#{hash}/events", + params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact + end + + # Returns all metrics for a particular report, designated by its unique hash. + # This endpoint does not currently support querying or paging. + # + # @param hash [String] The unique hash of the report. + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/reports.htm#pdbqueryv4reportshashmetrics + def metrics(hash:) + @client.get "#{BASE_PATH}/#{hash}/metrics" + end + + # Returns all logs for a particular report, designated by its unique hash. + # This endpoint does not currently support querying or paging. + # + # @param hash [String] The unique hash of the report. + # + # @return [Array] + # + # @see https://help.puppet.com/pdb/current/topics/reports.htm#pdbqueryv4reportshashlogs + def logs(hash:) + @client.get "#{BASE_PATH}/#{hash}/logs" + end + end + end + end + end +end diff --git a/sig/pe_client/client.rbs b/sig/pe_client/client.rbs index b89dde9..2154d9d 100644 --- a/sig/pe_client/client.rbs +++ b/sig/pe_client/client.rbs @@ -42,6 +42,8 @@ module PEClient def puppet_v3: -> Resource::PuppetV3 @puppet_ca_v1: Resource::PuppetCAV1 def puppet_ca_v1: -> Resource::PuppetCAV1 + @puppet_db: Resource::PuppetDB + def puppet_db: -> Resource::PuppetDB # Private methods def handle_response: (Faraday::Response, ?headers_only: bool) -> untyped diff --git a/sig/pe_client/resource/puppet_db.rbs b/sig/pe_client/resource/puppet_db.rbs new file mode 100644 index 0000000..8b29c19 --- /dev/null +++ b/sig/pe_client/resource/puppet_db.rbs @@ -0,0 +1,11 @@ +module PEClient + module Resource + class PuppetDB < BaseWithPort + BASE_PATH: String + PORT: Integer + + @query_v4: PuppetDB::QueryV4 + def query_v4: () -> PuppetDB::QueryV4 + end + end +end diff --git a/sig/pe_client/resource/puppet_db/query.v4.rbs b/sig/pe_client/resource/puppet_db/query.v4.rbs new file mode 100644 index 0000000..a5fa0da --- /dev/null +++ b/sig/pe_client/resource/puppet_db/query.v4.rbs @@ -0,0 +1,39 @@ +module PEClient + module Resource + class PuppetDB + class QueryV4 < Base + BASE_PATH: String + + def root: (query: Array[untyped], ?timeout: Integer?, ?ast_only: bool?, ?origin: String?, ?explain: String?, **untyped) -> Array[Hash[String, untyped]] + def environments: (?environment: String?, ?type: String?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def producers: (?producer: nil, ?type: nil, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + | (producer: String, ?type: nil, ?query: Array[untyped]?, **untyped) -> Hash[String, untyped] + | (producer: String, type: String, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def facts: (?fact_name: String?, ?value: String?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def fact_names: (?query: Array[untyped]?, **untyped) -> Array[String] + def fact_paths: (?query: Array[untyped]?, **untyped) -> Array[String] + def fact_contents: (?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def inventory: (?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def resources: (?type: String?, ?title: String?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def edges: (?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def events: (?distinct_resources: nil, ?distinct_start_time: nil, ?distinct_end_time: nil, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + | (distinct_resources: bool, ?distinct_start_time: String, ?distinct_end_time: String, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def event_counts: (summarize_by: String, ?count_by: String?, ?counts_filter: Array[untyped]?, ?distinct_resources: bool?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def aggregate_event_counts: (summarize_by: String, ?count_by: String?, ?counts_filter: Array[untyped]?, ?distinct_resources: bool?, ?query: Array[untyped]?) -> Array[Hash[String, untyped]] + def packages: (?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def package_inventory: (?certname: String?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + + @nodes: QueryV4::Nodes + def nodes: () -> QueryV4::Nodes + @factsets: QueryV4::Factsets + def factsets: () -> QueryV4::Factsets + @catalogs: QueryV4::Catalogs + def catalogs: () -> QueryV4::Catalogs + @reports: QueryV4::Reports + def reports: () -> QueryV4::Reports + + def self.query_paging: (**untyped) -> Hash[Symbol, untyped] + end + end + end +end \ No newline at end of file diff --git a/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs b/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs new file mode 100644 index 0000000..6fe4e18 --- /dev/null +++ b/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs @@ -0,0 +1,16 @@ +module PEClient + module Resource + class PuppetDB + class QueryV4 + class Catalogs < Base + BASE_PATH: String + + def get: (?node: nil, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + | (node: String, ?query: Array[untyped]?, **untyped) -> Hash[String, untyped] + def edges: (node: String, **untyped) -> Array[Hash[String, untyped]] + def resources: (node: String, **untyped) -> Array[Hash[String, untyped]] + end + end + end + end +end \ No newline at end of file diff --git a/sig/pe_client/resource/puppet_db/query.v4/factsets.rbs b/sig/pe_client/resource/puppet_db/query.v4/factsets.rbs new file mode 100644 index 0000000..d1487a1 --- /dev/null +++ b/sig/pe_client/resource/puppet_db/query.v4/factsets.rbs @@ -0,0 +1,14 @@ +module PEClient + module Resource + class PuppetDB + class QueryV4 + class Factsets < Base + BASE_PATH: String + + def get: (?node: String?, ?query: Array[untyped]?, **untyped) -> (Array[Hash[String, untyped]] | Hash[String, untyped]) + def facts: (node: String, **untyped) -> Array[Hash[String, untyped]] + end + end + end + end +end \ No newline at end of file diff --git a/sig/pe_client/resource/puppet_db/query.v4/nodes.rbs b/sig/pe_client/resource/puppet_db/query.v4/nodes.rbs new file mode 100644 index 0000000..efe30cc --- /dev/null +++ b/sig/pe_client/resource/puppet_db/query.v4/nodes.rbs @@ -0,0 +1,15 @@ +module PEClient + module Resource + class PuppetDB + class QueryV4 + class Nodes < Base + BASE_PATH: String + + def get: (?node: String?, ?query: Array[untyped]?, **untyped) -> (Array[Hash[String, untyped]] | Hash[String, untyped]) + def facts: (node: String, ?name: String?, ?value: String?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def resources: (node: String, ?type: String?, ?title: String?, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + end + end + end + end +end \ No newline at end of file diff --git a/sig/pe_client/resource/puppet_db/query.v4/reports.rbs b/sig/pe_client/resource/puppet_db/query.v4/reports.rbs new file mode 100644 index 0000000..c78b720 --- /dev/null +++ b/sig/pe_client/resource/puppet_db/query.v4/reports.rbs @@ -0,0 +1,16 @@ +module PEClient + module Resource + class PuppetDB + class QueryV4 + class Reports < Base + BASE_PATH: String + + def get: (?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def events: (hash: String, ?query: Array[untyped]?, **untyped) -> Array[Hash[String, untyped]] + def metrics: (hash: String) -> Array[Hash[String, untyped]] + def logs: (hash: String) -> Array[Hash[String, untyped]] + end + end + end + end +end \ No newline at end of file diff --git a/spec/pe_client/client_spec.rb b/spec/pe_client/client_spec.rb index 6bb2326..2787094 100644 --- a/spec/pe_client/client_spec.rb +++ b/spec/pe_client/client_spec.rb @@ -272,5 +272,6 @@ include_examples "a memoized resource", :puppet_admin_v1, "PEClient::Resource::PuppetAdminV1" include_examples "a memoized resource", :puppet_v3, "PEClient::Resource::PuppetV3" include_examples "a memoized resource", :puppet_ca_v1, "PEClient::Resource::PuppetCAV1" + include_examples "a memoized resource", :puppet_db, "PEClient::Resource::PuppetDB" end end diff --git a/spec/pe_client/resources/puppet_db/query_v4/catalogs_spec.rb b/spec/pe_client/resources/puppet_db/query_v4/catalogs_spec.rb new file mode 100644 index 0000000..1eadbca --- /dev/null +++ b/spec/pe_client/resources/puppet_db/query_v4/catalogs_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require "uri" +require_relative "../../../../../lib/pe_client/resources/puppet_db" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4/catalogs" + +RSpec.describe PEClient::Resource::PuppetDB::QueryV4::Catalogs do + let(:api_key) { "test_api_key" } + let(:base_url) { "https://puppet.example.com:8143" } + let(:client) { PEClient::Client.new(api_key: api_key, base_url: base_url, ca_file: nil) } + let(:puppetdb) { PEClient::Resource::PuppetDB.new(client) } + let(:resource) { described_class.new(puppetdb.instance_variable_get(:@client)) } + + describe "#get" do + it "retrieves all catalogs" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","version":"1234567890","producer_timestamp":"2025-01-01T00:00:00Z"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get + expect(response).to eq([{ + "certname" => "node1.example.com", + "version" => "1234567890", + "producer_timestamp" => "2025-01-01T00:00:00Z" + }]) + end + + it "retrieves catalog for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/node1.example.com") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '{"certname":"node1.example.com","version":"1234567890","environment":"production","edges":[],"resources":[]}', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(node: "node1.example.com") + expect(response).to eq({ + "certname" => "node1.example.com", + "version" => "1234567890", + "environment" => "production", + "edges" => [], + "resources" => [] + }) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["=", "certname", "node1.example.com"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(query: ["=", "certname", "node1.example.com"]) + expect(response).to eq([{"certname" => "node1.example.com"}]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "10", "offset" => "0"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(limit: 10, offset: 0) + expect(response).to eq([]) + end + end + + describe "#edges" do + it "retrieves edges for a specific catalog" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/node1.example.com/edges") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","source_type":"File","source_title":"/etc/hosts","target_type":"Service","target_title":"networking","relationship":"before"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.edges(node: "node1.example.com") + expect(response).to eq([{ + "certname" => "node1.example.com", + "source_type" => "File", + "source_title" => "/etc/hosts", + "target_type" => "Service", + "target_title" => "networking", + "relationship" => "before" + }]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "10"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/node1.example.com/edges?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.edges(node: "node1.example.com", limit: 10) + expect(response).to eq([]) + end + + it "returns edges even for deactivated nodes" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/deactivated.node.com/edges") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.edges(node: "deactivated.node.com") + expect(response).to eq([]) + end + end + + describe "#resources" do + it "retrieves resources for a specific catalog" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/node1.example.com/resources") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","type":"File","title":"/etc/hosts","parameters":{"ensure":"file"}}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com") + expect(response).to eq([{ + "certname" => "node1.example.com", + "type" => "File", + "title" => "/etc/hosts", + "parameters" => {"ensure" => "file"} + }]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "20"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/node1.example.com/resources?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com", limit: 20) + expect(response).to eq([]) + end + + it "returns resources even for deactivated nodes" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/catalogs/deactivated.node.com/resources") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"deactivated.node.com","type":"File","title":"/tmp/test"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "deactivated.node.com") + expect(response).to eq([{"certname" => "deactivated.node.com", "type" => "File", "title" => "/tmp/test"}]) + end + end +end diff --git a/spec/pe_client/resources/puppet_db/query_v4/factsets_spec.rb b/spec/pe_client/resources/puppet_db/query_v4/factsets_spec.rb new file mode 100644 index 0000000..7ead609 --- /dev/null +++ b/spec/pe_client/resources/puppet_db/query_v4/factsets_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "uri" +require_relative "../../../../../lib/pe_client/resources/puppet_db" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4/factsets" + +RSpec.describe PEClient::Resource::PuppetDB::QueryV4::Factsets do + let(:api_key) { "test_api_key" } + let(:base_url) { "https://puppet.example.com:8143" } + let(:client) { PEClient::Client.new(api_key: api_key, base_url: base_url, ca_file: nil) } + let(:puppetdb) { PEClient::Resource::PuppetDB.new(client) } + let(:resource) { described_class.new(puppetdb.instance_variable_get(:@client)) } + + describe "#get" do + it "retrieves all factsets" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","timestamp":"2025-01-01T00:00:00Z","facts":{"operatingsystem":"RedHat"}}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get + expect(response).to eq([{ + "certname" => "node1.example.com", + "timestamp" => "2025-01-01T00:00:00Z", + "facts" => {"operatingsystem" => "RedHat"} + }]) + end + + it "retrieves factset for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets/node1.example.com") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '{"certname":"node1.example.com","timestamp":"2025-01-01T00:00:00Z","facts":{"operatingsystem":"RedHat","kernel":"Linux"}}', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(node: "node1.example.com") + expect(response).to eq({ + "certname" => "node1.example.com", + "timestamp" => "2025-01-01T00:00:00Z", + "facts" => {"operatingsystem" => "RedHat", "kernel" => "Linux"} + }) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["=", "certname", "node1.example.com"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","facts":{}}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(query: ["=", "certname", "node1.example.com"]) + expect(response).to eq([{"certname" => "node1.example.com", "facts" => {}}]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "10", "offset" => "5"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(limit: 10, offset: 5) + expect(response).to eq([]) + end + end + + describe "#facts" do + it "retrieves facts for a specific factset" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets/node1.example.com/facts") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","name":"operatingsystem","value":"RedHat"},{"certname":"node1.example.com","name":"kernel","value":"Linux"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com") + expect(response).to eq([ + {"certname" => "node1.example.com", "name" => "operatingsystem", "value" => "RedHat"}, + {"certname" => "node1.example.com", "name" => "kernel", "value" => "Linux"} + ]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "5"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets/node1.example.com/facts?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com", limit: 5) + expect(response).to eq([]) + end + + it "returns facts even for deactivated nodes" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/factsets/deactivated.node.com/facts") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"deactivated.node.com","name":"operatingsystem","value":"Ubuntu"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "deactivated.node.com") + expect(response).to eq([{"certname" => "deactivated.node.com", "name" => "operatingsystem", "value" => "Ubuntu"}]) + end + end +end diff --git a/spec/pe_client/resources/puppet_db/query_v4/nodes_spec.rb b/spec/pe_client/resources/puppet_db/query_v4/nodes_spec.rb new file mode 100644 index 0000000..ef99774 --- /dev/null +++ b/spec/pe_client/resources/puppet_db/query_v4/nodes_spec.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +require "uri" +require_relative "../../../../../lib/pe_client/resources/puppet_db" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4/nodes" + +RSpec.describe PEClient::Resource::PuppetDB::QueryV4::Nodes do + let(:api_key) { "test_api_key" } + let(:base_url) { "https://puppet.example.com:8143" } + let(:client) { PEClient::Client.new(api_key: api_key, base_url: base_url, ca_file: nil) } + let(:puppetdb) { PEClient::Resource::PuppetDB.new(client) } + let(:resource) { described_class.new(puppetdb.instance_variable_get(:@client)) } + + describe "#get" do + it "retrieves all nodes" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","deactivated":null},{"certname":"node2.example.com","deactivated":null}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get + expect(response).to eq([ + {"certname" => "node1.example.com", "deactivated" => nil}, + {"certname" => "node2.example.com", "deactivated" => nil} + ]) + end + + it "retrieves a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '{"certname":"node1.example.com","deactivated":null,"catalog_timestamp":"2025-01-01T00:00:00Z"}', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(node: "node1.example.com") + expect(response).to eq({ + "certname" => "node1.example.com", + "deactivated" => nil, + "catalog_timestamp" => "2025-01-01T00:00:00Z" + }) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["~", "certname", "node.*"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(query: ["~", "certname", "node.*"]) + expect(response).to eq([{"certname" => "node1.example.com"}]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "10", "offset" => "0"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(limit: 10, offset: 0) + expect(response).to eq([]) + end + end + + describe "#facts" do + it "retrieves facts for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/facts") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com") + expect(response).to eq([{"certname" => "node1.example.com", "name" => "operatingsystem", "value" => "RedHat"}]) + end + + it "retrieves facts by name for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/facts/operatingsystem") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com", name: "operatingsystem") + expect(response).to eq([{"certname" => "node1.example.com", "name" => "operatingsystem", "value" => "RedHat"}]) + end + + it "retrieves facts by name and value for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/facts/operatingsystem/RedHat") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com", name: "operatingsystem", value: "RedHat") + expect(response).to eq([{"certname" => "node1.example.com", "name" => "operatingsystem", "value" => "RedHat"}]) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["~", "name", "operating.*"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/facts?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com", query: ["~", "name", "operating.*"]) + expect(response).to eq([]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "5"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/facts?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(node: "node1.example.com", limit: 5) + expect(response).to eq([]) + end + end + + describe "#resources" do + it "retrieves resources for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/resources") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","type":"File","title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com") + expect(response).to eq([{"certname" => "node1.example.com", "type" => "File", "title" => "/etc/hosts"}]) + end + + it "retrieves resources by type for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/resources/File") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","type":"File","title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com", type: "File") + expect(response).to eq([{"certname" => "node1.example.com", "type" => "File", "title" => "/etc/hosts"}]) + end + + it "retrieves resources by type and title for a specific node" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/resources/File//etc/hosts") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","type":"File","title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com", type: "File", title: "/etc/hosts") + expect(response).to eq([{"certname" => "node1.example.com", "type" => "File", "title" => "/etc/hosts"}]) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["=", "type", "File"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/resources?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com", query: ["=", "type", "File"]) + expect(response).to eq([]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "20"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/nodes/node1.example.com/resources?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(node: "node1.example.com", limit: 20) + expect(response).to eq([]) + end + end +end diff --git a/spec/pe_client/resources/puppet_db/query_v4/reports_spec.rb b/spec/pe_client/resources/puppet_db/query_v4/reports_spec.rb new file mode 100644 index 0000000..9d521f2 --- /dev/null +++ b/spec/pe_client/resources/puppet_db/query_v4/reports_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +require "uri" +require_relative "../../../../../lib/pe_client/resources/puppet_db" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4" +require_relative "../../../../../lib/pe_client/resources/puppet_db/query.v4/reports" + +RSpec.describe PEClient::Resource::PuppetDB::QueryV4::Reports do + let(:api_key) { "test_api_key" } + let(:base_url) { "https://puppet.example.com:8143" } + let(:client) { PEClient::Client.new(api_key: api_key, base_url: base_url, ca_file: nil) } + let(:puppetdb) { PEClient::Resource::PuppetDB.new(client) } + let(:resource) { described_class.new(puppetdb.instance_variable_get(:@client)) } + let(:report_hash) { "abc123def456" } + + describe "#get" do + it "retrieves all reports" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","hash":"abc123","start_time":"2025-01-01T00:00:00Z","end_time":"2025-01-01T00:30:00Z"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get + expect(response).to eq([{ + "certname" => "node1.example.com", + "hash" => "abc123", + "start_time" => "2025-01-01T00:00:00Z", + "end_time" => "2025-01-01T00:30:00Z" + }]) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["=", "certname", "node1.example.com"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","hash":"abc123"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(query: ["=", "certname", "node1.example.com"]) + expect(response).to eq([{"certname" => "node1.example.com", "hash" => "abc123"}]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "10", "offset" => "5"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(limit: 10, offset: 5) + expect(response).to eq([]) + end + + it "filters reports by status" do + query_param = URI.encode_www_form({"query" => ["=", "status", "success"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","status":"success"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.get(query: ["=", "status", "success"]) + expect(response).to eq([{"certname" => "node1.example.com", "status" => "success"}]) + end + end + + describe "#events" do + it "retrieves events for a specific report" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/events") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com","status":"success","timestamp":"2025-01-01T00:15:00Z","resource_type":"File","resource_title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.events(hash: report_hash) + expect(response).to eq([{ + "certname" => "node1.example.com", + "status" => "success", + "timestamp" => "2025-01-01T00:15:00Z", + "resource_type" => "File", + "resource_title" => "/etc/hosts" + }]) + end + + it "supports query parameter" do + query_param = URI.encode_www_form({"query" => ["=", "status", "success"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/events?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.events(hash: report_hash, query: ["=", "status", "success"]) + expect(response).to eq([]) + end + + it "supports paging parameters" do + query_param = URI.encode_www_form({"limit" => "50"}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/events?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.events(hash: report_hash, limit: 50) + expect(response).to eq([]) + end + + it "filters events by resource type" do + query_param = URI.encode_www_form({"query" => ["=", "resource_type", "File"].to_json}) + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/events?#{query_param}") + .with( + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"resource_type":"File","status":"success"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.events(hash: report_hash, query: ["=", "resource_type", "File"]) + expect(response).to eq([{"resource_type" => "File", "status" => "success"}]) + end + end + + describe "#metrics" do + it "retrieves metrics for a specific report" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/metrics") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"category":"resources","name":"total","value":150},{"category":"time","name":"config_retrieval","value":5.5}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.metrics(hash: report_hash) + expect(response).to eq([ + {"category" => "resources", "name" => "total", "value" => 150}, + {"category" => "time", "name" => "config_retrieval", "value" => 5.5} + ]) + end + + it "returns empty array when no metrics are available" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/metrics") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.metrics(hash: report_hash) + expect(response).to eq([]) + end + + it "returns metrics with various categories" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/metrics") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"category":"resources","name":"changed","value":5},{"category":"resources","name":"failed","value":0},{"category":"events","name":"success","value":5}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.metrics(hash: report_hash) + expect(response).to eq([ + {"category" => "resources", "name" => "changed", "value" => 5}, + {"category" => "resources", "name" => "failed", "value" => 0}, + {"category" => "events", "name" => "success", "value" => 5} + ]) + end + end + + describe "#logs" do + it "retrieves logs for a specific report" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/logs") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"level":"info","message":"Applied catalog in 30.5 seconds","source":"Puppet","time":"2025-01-01T00:30:00Z"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.logs(hash: report_hash) + expect(response).to eq([{ + "level" => "info", + "message" => "Applied catalog in 30.5 seconds", + "source" => "Puppet", + "time" => "2025-01-01T00:30:00Z" + }]) + end + + it "returns empty array when no logs are available" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/logs") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: "[]", + headers: {"Content-Type" => "application/json"} + ) + + response = resource.logs(hash: report_hash) + expect(response).to eq([]) + end + + it "returns logs with various levels" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/reports/#{report_hash}/logs") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"level":"notice","message":"Notice message"},{"level":"warning","message":"Warning message"},{"level":"err","message":"Error message"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.logs(hash: report_hash) + expect(response).to eq([ + {"level" => "notice", "message" => "Notice message"}, + {"level" => "warning", "message" => "Warning message"}, + {"level" => "err", "message" => "Error message"} + ]) + end + end +end diff --git a/spec/pe_client/resources/puppet_db/query_v4_spec.rb b/spec/pe_client/resources/puppet_db/query_v4_spec.rb new file mode 100644 index 0000000..6b46ae6 --- /dev/null +++ b/spec/pe_client/resources/puppet_db/query_v4_spec.rb @@ -0,0 +1,584 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/pe_client/resources/puppet_db" +require_relative "../../../../lib/pe_client/resources/puppet_db/query.v4" + +RSpec.describe PEClient::Resource::PuppetDB::QueryV4 do + let(:api_key) { "test_api_key" } + let(:base_url) { "https://puppet.example.com:8143" } + let(:client) { PEClient::Client.new(api_key: api_key, base_url: base_url, ca_file: nil) } + let(:puppetdb) { PEClient::Resource::PuppetDB.new(client) } + let(:resource) { described_class.new(puppetdb.instance_variable_get(:@client)) } + subject { resource } + + describe "#root" do + it "performs an AST query with required parameters" do + query_array = ["from", "nodes", ["=", "certname", "node1.example.com"]] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4") + .with( + query: hash_including( + "query" => query_array.to_json + ), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.root(query: query_array) + expect(response).to eq([{"certname" => "node1.example.com"}]) + end + + it "performs an AST query with all optional parameters" do + query_array = ["from", "nodes", ["=", "certname", "node1.example.com"]] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4") + .with( + query: hash_including( + "query" => query_array.to_json, + "limit" => "10", + "offset" => "5", + "timeout" => "30", + "include_total" => "true" + ), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1.example.com"}]', + headers: {"Content-Type" => "application/json", "X-Records" => "100"} + ) + + response = resource.root( + query: query_array, + limit: 10, + offset: 5, + timeout: 30, + include_total: true + ) + expect(response).to eq([{"certname" => "node1.example.com"}]) + end + end + + describe "#environments" do + it "retrieves environments with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/environments") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"name":"production"},{"name":"development"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.environments + expect(response).to eq([{"name" => "production"}, {"name" => "development"}]) + end + + it "retrieves environments with environment parameter" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/environments/production") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"name":"production"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.environments(environment: "production") + expect(response).to eq([{"name" => "production"}]) + end + + it "retrieves environments with environment and type parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/environments/production/events") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"name":"production"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.environments(environment: "production", type: "events") + expect(response).to eq([{"name" => "production"}]) + end + end + + describe "#producers" do + it "retrieves producers with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/producers") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"name":"puppet-server1"},{"name":"puppet-server2"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.producers + expect(response).to eq([{"name" => "puppet-server1"}, {"name" => "puppet-server2"}]) + end + + it "retrieves producers with producer parameter" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/producers/puppet-server1") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '{"name":"puppet-server1"}', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.producers(producer: "puppet-server1") + expect(response).to eq({"name" => "puppet-server1"}) + end + + it "retrieves producers with producer and type parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/producers/puppet-server1/catalogs") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"name":"puppet-server1"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.producers(producer: "puppet-server1", type: "catalogs") + expect(response).to eq([{"name" => "puppet-server1"}]) + end + end + + describe "#facts" do + it "retrieves facts with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/facts") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts + expect(response).to eq([{"certname" => "node1", "name" => "operatingsystem", "value" => "RedHat"}]) + end + + it "retrieves facts with fact_name parameter" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/facts/operatingsystem") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(fact_name: "operatingsystem") + expect(response).to eq([{"certname" => "node1", "name" => "operatingsystem", "value" => "RedHat"}]) + end + + it "retrieves facts with fact_name and value parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/facts/operatingsystem/RedHat") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(fact_name: "operatingsystem", value: "RedHat") + expect(response).to eq([{"certname" => "node1", "name" => "operatingsystem", "value" => "RedHat"}]) + end + end + + describe "#fact_names" do + it "retrieves fact names with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/fact-names") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '["operatingsystem","kernel","architecture"]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.fact_names + expect(response).to eq(["operatingsystem", "kernel", "architecture"]) + end + + it "retrieves fact names with all optional parameters" do + query_array = ["~", "name", "operating.*"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/fact-names") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '["operatingsystem"]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.fact_names(query: query_array) + expect(response).to eq(["operatingsystem"]) + end + end + + describe "#fact_paths" do + it "retrieves fact paths with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/fact-paths") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '["operatingsystem","networking.interfaces.eth0.mac"]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.fact_paths + expect(response).to eq(["operatingsystem", "networking.interfaces.eth0.mac"]) + end + + it "retrieves fact paths with all optional parameters" do + query_array = ["~", "path", "networking.*"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/fact-paths") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '["networking.interfaces.eth0.mac"]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.fact_paths(query: query_array) + expect(response).to eq(["networking.interfaces.eth0.mac"]) + end + end + + describe "#fact_contents" do + it "retrieves fact contents with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/fact-contents") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","path":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.fact_contents + expect(response).to eq([{"certname" => "node1", "path" => "operatingsystem", "value" => "RedHat"}]) + end + + it "retrieves fact contents with all optional parameters" do + query_array = ["=", "path", "operatingsystem"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/fact-contents") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1","path":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.fact_contents(query: query_array) + expect(response).to eq([{"certname" => "node1", "path" => "operatingsystem", "value" => "RedHat"}]) + end + end + + describe "#inventory" do + it "retrieves inventory data with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/inventory") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","environment":"production","facts":{"operatingsystem":"RedHat"}}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.inventory + expect(response).to eq([{"certname" => "node1", "environment" => "production", "facts" => {"operatingsystem" => "RedHat"}}]) + end + + it "retrieves inventory data with all optional parameters" do + query_array = ["=", "certname", "node1"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/inventory") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1","environment":"production","facts":{"operatingsystem":"RedHat"}}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.inventory(query: query_array) + expect(response).to eq([{"certname" => "node1", "environment" => "production", "facts" => {"operatingsystem" => "RedHat"}}]) + end + end + + describe "#resources" do + it "retrieves resources with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/resources") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","type":"File","title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources + expect(response).to eq([{"certname" => "node1", "type" => "File", "title" => "/etc/hosts"}]) + end + + it "retrieves resources with type parameter" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/resources/File") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","type":"File","title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(type: "File") + expect(response).to eq([{"certname" => "node1", "type" => "File", "title" => "/etc/hosts"}]) + end + + it "retrieves resources with type and title parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/resources/File/%2Fetc%2Fhosts") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","type":"File","title":"/etc/hosts"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.resources(type: "File", title: "/etc/hosts") + expect(response).to eq([{"certname" => "node1", "type" => "File", "title" => "/etc/hosts"}]) + end + end + + describe "#edges" do + it "retrieves edges with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/edges") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","source_type":"File","source_title":"/etc/hosts","target_type":"Service","target_title":"networking"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.edges + expect(response).to eq([{"certname" => "node1", "source_type" => "File", "source_title" => "/etc/hosts", "target_type" => "Service", "target_title" => "networking"}]) + end + + it "retrieves edges with all optional parameters" do + query_array = ["=", "certname", "node1"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/edges") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1","source_type":"File","source_title":"/etc/hosts","target_type":"Service","target_title":"networking"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.edges(query: query_array) + expect(response).to eq([{"certname" => "node1", "source_type" => "File", "source_title" => "/etc/hosts", "target_type" => "Service", "target_title" => "networking"}]) + end + end + + describe "#events" do + it "retrieves events with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/events") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","status":"success","timestamp":"2025-01-01T00:00:00Z"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.events + expect(response).to eq([{"certname" => "node1", "status" => "success", "timestamp" => "2025-01-01T00:00:00Z"}]) + end + + it "retrieves events with all optional parameters" do + query_array = ["=", "status", "success"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/events") + .with( + query: hash_including( + "query" => query_array.to_json, + "distinct_resources" => "true", + "distinct_start_time" => "2025-01-01T00:00:00Z", + "distinct_end_time" => "2025-01-02T00:00:00Z" + ), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1","status":"success","timestamp":"2025-01-01T00:00:00Z"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.events( + query: query_array, + distinct_resources: true, + distinct_start_time: "2025-01-01T00:00:00Z", + distinct_end_time: "2025-01-02T00:00:00Z" + ) + expect(response).to eq([{"certname" => "node1", "status" => "success", "timestamp" => "2025-01-01T00:00:00Z"}]) + end + end + + describe "#event_counts" do + it "retrieves event counts with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/event-counts") + .with( + query: hash_including("summarize_by" => "certname"), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"subject":"node1","successes":1,"failures":0}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.event_counts(summarize_by: "certname") + expect(response).to eq([{"subject" => "node1", "successes" => 1, "failures" => 0}]) + end + + it "retrieves event counts with all optional parameters" do + counts_filter_array = ["=", "status", "success"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/event-counts") + .with( + query: hash_including( + "summarize_by" => "certname", + "count_by" => "certname", + "counts_filter" => counts_filter_array.to_json, + "distinct_resources" => "true" + ), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"subject":"node1","successes":1,"failures":0}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.event_counts( + summarize_by: "certname", + count_by: "certname", + counts_filter: counts_filter_array, + distinct_resources: true + ) + expect(response).to eq([{"subject" => "node1", "successes" => 1, "failures" => 0}]) + end + end + + describe "#aggregate_event_counts" do + it "retrieves aggregate event counts with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/aggregate-event-counts") + .with( + query: hash_including("summarize_by" => "certname"), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"successes":50,"failures":2}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.aggregate_event_counts(summarize_by: "certname") + expect(response).to eq([{"successes" => 50, "failures" => 2}]) + end + + it "retrieves aggregate event counts with all optional parameters" do + counts_filter_array = ["=", "status", "success"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/aggregate-event-counts") + .with( + query: hash_including( + "summarize_by" => "certname", + "count_by" => "certname", + "counts_filter" => counts_filter_array.to_json, + "distinct_resources" => "true" + ), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"successes":50,"failures":2}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.aggregate_event_counts( + summarize_by: "certname", + count_by: "certname", + counts_filter: counts_filter_array, + distinct_resources: true + ) + expect(response).to eq([{"successes" => 50, "failures" => 2}]) + end + end + + describe "#packages" do + it "retrieves packages with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/packages") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"package_name":"httpd","version":"2.4.6"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.packages + expect(response).to eq([{"package_name" => "httpd", "version" => "2.4.6"}]) + end + + it "retrieves packages with all optional parameters" do + query_array = ["=", "package_name", "httpd"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/packages") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"package_name":"httpd","version":"2.4.6"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.packages(query: query_array) + expect(response).to eq([{"package_name" => "httpd", "version" => "2.4.6"}]) + end + end + + describe "#package_inventory" do + it "retrieves package inventory with required parameters" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/package-inventory") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","package_name":"httpd","version":"2.4.6"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.package_inventory + expect(response).to eq([{"certname" => "node1", "package_name" => "httpd", "version" => "2.4.6"}]) + end + + it "retrieves package inventory with certname parameter" do + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/package-inventory/node1") + .with(headers: {"X-Authentication" => api_key}) + .to_return( + status: 200, + body: '[{"certname":"node1","package_name":"httpd","version":"2.4.6"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.package_inventory(certname: "node1") + expect(response).to eq([{"certname" => "node1", "package_name" => "httpd", "version" => "2.4.6"}]) + end + end + + include_examples "a memoized resource", :nodes, "PEClient::Resource::PuppetDB::QueryV4::Nodes" + include_examples "a memoized resource", :factsets, "PEClient::Resource::PuppetDB::QueryV4::Factsets" + include_examples "a memoized resource", :catalogs, "PEClient::Resource::PuppetDB::QueryV4::Catalogs" + include_examples "a memoized resource", :reports, "PEClient::Resource::PuppetDB::QueryV4::Reports" +end diff --git a/spec/pe_client/resources/puppet_db_spec.rb b/spec/pe_client/resources/puppet_db_spec.rb new file mode 100644 index 0000000..f5be1ef --- /dev/null +++ b/spec/pe_client/resources/puppet_db_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "../../../lib/pe_client/resources/puppet_db" + +RSpec.describe PEClient::Resource::PuppetDB do + let(:api_key) { "test_api_key" } + let(:base_url) { "https://puppet.example.com:8143" } + let(:client) { PEClient::Client.new(api_key: api_key, base_url: base_url, ca_file: nil) } + subject(:resource) { described_class.new(client) } + + include_examples "a resource with port", 8080 + + include_examples "a memoized resource", :query_v4, "PEClient::Resource::PuppetDB::QueryV4" +end