From 1e8d1be88f17efa70c449563d10b81f0bac64204 Mon Sep 17 00:00:00 2001 From: Zach Bensley Date: Mon, 12 Jan 2026 10:36:25 +0800 Subject: [PATCH 1/5] feat: add PuppetDB Query V4 APIs --- lib/pe_client/client.rb | 6 + lib/pe_client/resources/puppet_db.rb | 38 ++ lib/pe_client/resources/puppet_db/query.v4.rb | 371 ++++++++++++ .../resources/puppet_db/query.v4/catalogs.rb | 76 +++ .../resources/puppet_db/query.v4/factsets.rb | 65 +++ .../resources/puppet_db/query.v4/nodes.rb | 96 +++ .../resources/puppet_db/query.v4/reports.rb | 91 +++ sig/pe_client/resource/puppet_db.rbs | 11 + sig/pe_client/resource/puppet_db/query.v4.rbs | 39 ++ .../resource/puppet_db/query.v4/catalogs.rbs | 15 + .../resource/puppet_db/query.v4/factsets.rbs | 14 + .../resource/puppet_db/query.v4/nodes.rbs | 15 + .../resource/puppet_db/query.v4/reports.rbs | 16 + .../puppet_db/query_v4/catalogs_spec.rb | 184 ++++++ .../puppet_db/query_v4/factsets_spec.rb | 129 +++++ .../puppet_db/query_v4/nodes_spec.rb | 227 ++++++++ .../puppet_db/query_v4/reports_spec.rb | 250 ++++++++ .../resources/puppet_db/query_v4_spec.rb | 548 ++++++++++++++++++ spec/pe_client/resources/puppet_db_spec.rb | 14 + 19 files changed, 2205 insertions(+) create mode 100644 lib/pe_client/resources/puppet_db.rb create mode 100644 lib/pe_client/resources/puppet_db/query.v4.rb create mode 100644 lib/pe_client/resources/puppet_db/query.v4/catalogs.rb create mode 100644 lib/pe_client/resources/puppet_db/query.v4/factsets.rb create mode 100644 lib/pe_client/resources/puppet_db/query.v4/nodes.rb create mode 100644 lib/pe_client/resources/puppet_db/query.v4/reports.rb create mode 100644 sig/pe_client/resource/puppet_db.rbs create mode 100644 sig/pe_client/resource/puppet_db/query.v4.rbs create mode 100644 sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs create mode 100644 sig/pe_client/resource/puppet_db/query.v4/factsets.rbs create mode 100644 sig/pe_client/resource/puppet_db/query.v4/nodes.rbs create mode 100644 sig/pe_client/resource/puppet_db/query.v4/reports.rbs create mode 100644 spec/pe_client/resources/puppet_db/query_v4/catalogs_spec.rb create mode 100644 spec/pe_client/resources/puppet_db/query_v4/factsets_spec.rb create mode 100644 spec/pe_client/resources/puppet_db/query_v4/nodes_spec.rb create mode 100644 spec/pe_client/resources/puppet_db/query_v4/reports_spec.rb create mode 100644 spec/pe_client/resources/puppet_db/query_v4_spec.rb create mode 100644 spec/pe_client/resources/puppet_db_spec.rb 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..1c725b9 --- /dev/null +++ b/lib/pe_client/resources/puppet_db/query.v4.rb @@ -0,0 +1,371 @@ +# 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 represntation 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] + 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 += "/#{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..8bf31c4 --- /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..803ebc1 --- /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 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/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..47d0295 --- /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: bool?, **untyped) -> Array[Hash[String, untyped]] + def environments: (?environment: String?, ?type: String?, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def producers: (?producer: nil, ?type: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + | (producer: String, ?type: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Hash[String, untyped] + | (producer: String, type: String, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def facts: (?fact_name: String?, ?value: String?, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def fact_names: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[String] + def fact_paths: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[String] + def fact_contents: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def inventory: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def resources: (?type: String?, ?title: String?, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def edges: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def events: (?distinct_resources: nil, ?distinct_start_time: nil, ?distinct_end_time: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + | (distinct_resources: bool, ?distinct_start_time: String, ?distinct_end_time: String, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def event_counts: (summarize_by: String, ?count_by: String?, ?counts_filter: Array[untyped]?, ?distinct_resources: bool?, ?query: Array[untyped]?, **Hash[Symbol, 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]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] + def package_inventory: (?certname: String?, ?query: Array[untyped]?, **Hash[Symbol, 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 query_paging: (**Hash[Symbol, 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..c0c0376 --- /dev/null +++ b/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs @@ -0,0 +1,15 @@ +module PEClient + module Resource + class PuppetDB + class QueryV4 + class Catalogs < Base + BASE_PATH: String + + def get: (?node: String?, ?query: Array[untyped]?, **untyped) -> (Array[Hash[String, 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/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..3f205e0 --- /dev/null +++ b/spec/pe_client/resources/puppet_db/query_v4_spec.rb @@ -0,0 +1,548 @@ +# 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 all optional parameters" do + query_array = ["=", "name", "production"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/environments") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"name":"production"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.environments(query: query_array) + 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 all optional 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"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.producers + 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 all optional parameters" do + query_array = ["=", "name", "operatingsystem"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/facts") + .with( + query: hash_including("query" => query_array.to_json), + headers: {"X-Authentication" => api_key} + ) + .to_return( + status: 200, + body: '[{"certname":"node1","name":"operatingsystem","value":"RedHat"}]', + headers: {"Content-Type" => "application/json"} + ) + + response = resource.facts(query: query_array) + 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 all optional parameters" do + query_array = ["=", "type", "File"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/resources") + .with( + query: hash_including("query" => query_array.to_json), + 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(query: query_array) + 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 all optional parameters" do + query_array = ["=", "certname", "node1"] + stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/package-inventory") + .with( + query: hash_including("query" => query_array.to_json), + 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(query: query_array) + 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 From a0963c1ddb074d97b800a0ea498083172d433aa8 Mon Sep 17 00:00:00 2001 From: Zach Bensley Date: Mon, 12 Jan 2026 10:52:38 +0800 Subject: [PATCH 2/5] fix: resolve various documentation issues --- lib/pe_client/resources/puppet_db/query.v4.rb | 2 +- lib/pe_client/resources/puppet_db/query.v4/factsets.rb | 8 ++++---- lib/pe_client/resources/puppet_db/query.v4/nodes.rb | 8 ++++---- sig/pe_client/resource/puppet_db/query.v4.rbs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/pe_client/resources/puppet_db/query.v4.rb b/lib/pe_client/resources/puppet_db/query.v4.rb index 1c725b9..3b28776 100644 --- a/lib/pe_client/resources/puppet_db/query.v4.rb +++ b/lib/pe_client/resources/puppet_db/query.v4.rb @@ -122,7 +122,7 @@ def producers(producer: nil, type: nil, query: nil, **kwargs) @client.get uri, params: {query: query&.to_json}.merge!(QueryV4.query_paging(**kwargs)).compact end - # The facts endpoint provides access to a represntation of node factsets where a result is returned for each top-level key in the node's structured factset. + # 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. diff --git a/lib/pe_client/resources/puppet_db/query.v4/factsets.rb b/lib/pe_client/resources/puppet_db/query.v4/factsets.rb index 8bf31c4..ba7d7d2 100644 --- a/lib/pe_client/resources/puppet_db/query.v4/factsets.rb +++ b/lib/pe_client/resources/puppet_db/query.v4/factsets.rb @@ -36,7 +36,7 @@ class Factsets < Base # # @return [Array, Hash] # - # @see {QueryV4.query_paging} for paging options + # @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) @@ -45,15 +45,15 @@ def get(node: nil, query: nil, **kwargs) 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. + # 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 {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 diff --git a/lib/pe_client/resources/puppet_db/query.v4/nodes.rb b/lib/pe_client/resources/puppet_db/query.v4/nodes.rb index 803ebc1..0297029 100644 --- a/lib/pe_client/resources/puppet_db/query.v4/nodes.rb +++ b/lib/pe_client/resources/puppet_db/query.v4/nodes.rb @@ -45,8 +45,8 @@ def get(node: nil, query: nil, **kwargs) 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", ""]. + # 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] @@ -69,8 +69,8 @@ def facts(node:, name: nil, value: nil, query: nil, **kwargs) # 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", ""]. + # 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. diff --git a/sig/pe_client/resource/puppet_db/query.v4.rbs b/sig/pe_client/resource/puppet_db/query.v4.rbs index 47d0295..86f505a 100644 --- a/sig/pe_client/resource/puppet_db/query.v4.rbs +++ b/sig/pe_client/resource/puppet_db/query.v4.rbs @@ -4,7 +4,7 @@ module PEClient class QueryV4 < Base BASE_PATH: String - def root: (query: Array[untyped], ?timeout: Integer?, ?ast_only: bool?, ?origin: String?, ?explain: bool?, **untyped) -> Array[Hash[String, untyped]] + 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]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] def producers: (?producer: nil, ?type: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] | (producer: String, ?type: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Hash[String, untyped] @@ -32,7 +32,7 @@ module PEClient @reports: QueryV4::Reports def reports: () -> QueryV4::Reports - def query_paging: (**Hash[Symbol, untyped]) -> Hash[Symbol, untyped] + def self.query_paging: (**untyped) -> Hash[Symbol, untyped] end end end From 2b23079cdb280d700ee042613c27703690f7c200 Mon Sep 17 00:00:00 2001 From: Zach Bensley Date: Mon, 12 Jan 2026 13:23:05 +0800 Subject: [PATCH 3/5] fix: added missing @see tag fixed `resource` URI generation fixed working of `Nodes` class documentation fixed missing RBS definition fixed kwargs type definition added missing URI construction tests --- lib/pe_client/resources/puppet_db/query.v4.rb | 4 +- .../resources/puppet_db/query.v4/nodes.rb | 2 +- sig/pe_client/client.rbs | 2 + sig/pe_client/resource/puppet_db/query.v4.rbs | 32 +++--- .../resource/puppet_db/query.v4/catalogs.rbs | 3 +- spec/pe_client/client_spec.rb | 1 + .../resources/puppet_db/query_v4_spec.rb | 100 ++++++++++++------ 7 files changed, 93 insertions(+), 51 deletions(-) diff --git a/lib/pe_client/resources/puppet_db/query.v4.rb b/lib/pe_client/resources/puppet_db/query.v4.rb index 3b28776..3377ca8 100644 --- a/lib/pe_client/resources/puppet_db/query.v4.rb +++ b/lib/pe_client/resources/puppet_db/query.v4.rb @@ -185,6 +185,8 @@ def fact_contents(query: nil, **kwargs) # @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 @@ -202,7 +204,7 @@ def inventory(query: nil, **kwargs) def resources(type: nil, title: nil, query: nil, **kwargs) uri = "#{BASE_PATH}/resources" uri += "/#{type}" if type - uri += "/#{title}" if type && title + 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 diff --git a/lib/pe_client/resources/puppet_db/query.v4/nodes.rb b/lib/pe_client/resources/puppet_db/query.v4/nodes.rb index 0297029..8985021 100644 --- a/lib/pe_client/resources/puppet_db/query.v4/nodes.rb +++ b/lib/pe_client/resources/puppet_db/query.v4/nodes.rb @@ -20,7 +20,7 @@ module PEClient module Resource class PuppetDB class QueryV4 - # Nodes can be queried by making through these endpoints. + # Nodes can be queried by making requests through these endpoints. # # @see https://help.puppet.com/pdb/current/topics/nodes.htm class Nodes < Base 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/query.v4.rbs b/sig/pe_client/resource/puppet_db/query.v4.rbs index 86f505a..a5fa0da 100644 --- a/sig/pe_client/resource/puppet_db/query.v4.rbs +++ b/sig/pe_client/resource/puppet_db/query.v4.rbs @@ -5,23 +5,23 @@ module PEClient 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]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def producers: (?producer: nil, ?type: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - | (producer: String, ?type: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Hash[String, untyped] - | (producer: String, type: String, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def facts: (?fact_name: String?, ?value: String?, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def fact_names: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[String] - def fact_paths: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[String] - def fact_contents: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def inventory: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def resources: (?type: String?, ?title: String?, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def edges: (?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def events: (?distinct_resources: nil, ?distinct_start_time: nil, ?distinct_end_time: nil, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - | (distinct_resources: bool, ?distinct_start_time: String, ?distinct_end_time: String, ?query: Array[untyped]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def event_counts: (summarize_by: String, ?count_by: String?, ?counts_filter: Array[untyped]?, ?distinct_resources: bool?, ?query: Array[untyped]?, **Hash[Symbol, 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]?, **Hash[Symbol, untyped]) -> Array[Hash[String, untyped]] - def package_inventory: (?certname: String?, ?query: Array[untyped]?, **Hash[Symbol, 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 diff --git a/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs b/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs index c0c0376..6fe4e18 100644 --- a/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs +++ b/sig/pe_client/resource/puppet_db/query.v4/catalogs.rbs @@ -5,7 +5,8 @@ module PEClient class Catalogs < Base BASE_PATH: String - def get: (?node: String?, ?query: Array[untyped]?, **untyped) -> (Array[Hash[String, untyped]] | Hash[String, untyped]) + 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 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_spec.rb b/spec/pe_client/resources/puppet_db/query_v4_spec.rb index 3f205e0..6b46ae6 100644 --- a/spec/pe_client/resources/puppet_db/query_v4_spec.rb +++ b/spec/pe_client/resources/puppet_db/query_v4_spec.rb @@ -75,20 +75,29 @@ expect(response).to eq([{"name" => "production"}, {"name" => "development"}]) end - it "retrieves environments with all optional parameters" do - query_array = ["=", "name", "production"] - stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/environments") - .with( - query: hash_including("query" => query_array.to_json), - headers: {"X-Authentication" => api_key} + 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(query: query_array) + response = resource.environments(environment: "production", type: "events") expect(response).to eq([{"name" => "production"}]) end end @@ -107,8 +116,21 @@ expect(response).to eq([{"name" => "puppet-server1"}, {"name" => "puppet-server2"}]) end - it "retrieves producers with all optional parameters" do - stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/producers") + 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, @@ -116,7 +138,7 @@ headers: {"Content-Type" => "application/json"} ) - response = resource.producers + response = resource.producers(producer: "puppet-server1", type: "catalogs") expect(response).to eq([{"name" => "puppet-server1"}]) end end @@ -135,20 +157,29 @@ expect(response).to eq([{"certname" => "node1", "name" => "operatingsystem", "value" => "RedHat"}]) end - it "retrieves facts with all optional parameters" do - query_array = ["=", "name", "operatingsystem"] - stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/facts") - .with( - query: hash_including("query" => query_array.to_json), - headers: {"X-Authentication" => api_key} + 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(query: query_array) + response = resource.facts(fact_name: "operatingsystem", value: "RedHat") expect(response).to eq([{"certname" => "node1", "name" => "operatingsystem", "value" => "RedHat"}]) end end @@ -295,20 +326,29 @@ expect(response).to eq([{"certname" => "node1", "type" => "File", "title" => "/etc/hosts"}]) end - it "retrieves resources with all optional parameters" do - query_array = ["=", "type", "File"] - stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/resources") - .with( - query: hash_including("query" => query_array.to_json), - headers: {"X-Authentication" => api_key} + 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(query: query_array) + response = resource.resources(type: "File", title: "/etc/hosts") expect(response).to eq([{"certname" => "node1", "type" => "File", "title" => "/etc/hosts"}]) end end @@ -523,20 +563,16 @@ expect(response).to eq([{"certname" => "node1", "package_name" => "httpd", "version" => "2.4.6"}]) end - it "retrieves package inventory with all optional parameters" do - query_array = ["=", "certname", "node1"] - stub_request(:get, "https://puppet.example.com:8080/pdb/query/v4/package-inventory") - .with( - query: hash_including("query" => query_array.to_json), - headers: {"X-Authentication" => api_key} - ) + 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(query: query_array) + response = resource.package_inventory(certname: "node1") expect(response).to eq([{"certname" => "node1", "package_name" => "httpd", "version" => "2.4.6"}]) end end From fe71c1804e8d3153dac4d239f5a57e80add9c0a1 Mon Sep 17 00:00:00 2001 From: Zach Bensley Date: Fri, 20 Feb 2026 12:58:14 +0800 Subject: [PATCH 4/5] fix: update Gemfile with fixed version of yard-lint --- Gemfile.lock | 62 ++++++++++++++++++++++++++--------------------- pe-client.gemspec | 2 +- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 79efd6a..eb2b5fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: pe_client (0.1.0) faraday (~> 2.14) + faraday-multipart (~> 1.2) GEM remote: https://rubygems.org/ @@ -17,32 +18,36 @@ GEM date (3.5.1) diff-lcs (1.6.2) erb (6.0.1) - faraday (2.14.0) + faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) json logger + faraday-multipart (1.2.0) + multipart-post (~> 2.0) faraday-net_http (3.4.2) net-http (~> 0.5) hashdiff (1.2.1) io-console (0.8.2) - irb (1.16.0) + irb (1.17.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.18.0) + json (2.18.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) + multipart-post (2.4.1) net-http (0.9.1) uri (>= 0.11.1) parallel (1.27.0) - parser (3.3.10.0) + parser (3.3.10.2) ast (~> 2.4.1) racc pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.7.0) + prism (1.9.0) psych (5.3.1) date stringio @@ -50,9 +55,10 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.3.1) - rbs (3.10.0) + rbs (3.10.3) logger - rdoc (7.0.3) + tsort + rdoc (7.2.0) erb psych (>= 4.0.0) tsort @@ -74,8 +80,8 @@ GEM rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.6) - rubocop (1.81.7) + rspec-support (3.13.7) + rubocop (1.84.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -83,7 +89,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) @@ -94,10 +100,10 @@ GEM rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (1.13.0) - standard (1.52.0) + standard (1.54.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.81.7) + rubocop (~> 1.84.0) standard-custom (~> 1.0.0) standard-performance (~> 1.8) standard-custom (1.0.2) @@ -117,10 +123,10 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) yard (0.9.38) - yard-lint (1.3.0) + yard-lint (1.4.0) yard (~> 0.9) zeitwerk (~> 2.6) - zeitwerk (2.7.4) + zeitwerk (2.7.5) PLATFORMS arm64-darwin-25 @@ -135,7 +141,7 @@ DEPENDENCIES rspec-github (~> 3.0) standard (~> 1.52) webmock (~> 3.26) - yard-lint (~> 1.2) + yard-lint (~> 1.4) CHECKSUMS addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057 @@ -145,29 +151,31 @@ CHECKSUMS date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 - faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd + faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc - irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806 - json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505 + irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae + json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 - parser (3.3.10.0) sha256=ce3587fa5cc55a88c4ba5b2b37621b3329aadf5728f9eafa36bbd121462aabd6 + parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 pe_client (0.1.0) pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 - prism (1.7.0) sha256=10062f734bf7985c8424c44fac382ac04a58124ea3d220ec3ba9fe4f2da65103 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c - rbs (3.10.0) sha256=e75b5f1313c71c9ee0fcea68bf97d3e5fe8ec7a641d4b5cd18bbc28c94ddf298 - rdoc (7.0.3) sha256=dfe3d0981d19b7bba71d9dbaeb57c9f4e3a7a4103162148a559c4fc687ea81f9 + rbs (3.10.3) sha256=70627f3919016134d554e6c99195552ae3ef6020fe034c8e983facc9c192daa6 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 @@ -176,12 +184,12 @@ CHECKSUMS rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 rspec-github (3.0.0) sha256=46af3cd8cad3e4434c20598752b6e721c5325e73d7e36e256379f0d3a85968af rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c - rspec-support (3.13.6) sha256=2e8de3702427eab064c9352fe74488cc12a1bfae887ad8b91cba480ec9f8afb2 - rubocop (1.81.7) sha256=6fb5cc298c731691e2a414fe0041a13eb1beed7bab23aec131da1bcc527af094 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 - standard (1.52.0) sha256=ec050e63228e31fabe40da3ef96da7edda476f7acdf3e7c2ad47b6e153f6a076 + standard (1.54.0) sha256=7a4b08f83d9893083c8f03bc486f0feeb6a84d48233b40829c03ef4767ea0100 standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 @@ -191,8 +199,8 @@ CHECKSUMS uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 webmock (3.26.1) sha256=4f696fb57c90a827c20aadb2d4f9058bbff10f7f043bd0d4c3f58791143b1cd7 yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f - yard-lint (1.3.0) sha256=473aa7b4d5632af873ae66bbd056b07827d81e9340f83a07a123123da6b1c25c - zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b + yard-lint (1.4.0) sha256=7dd88fbb08fd77cb840bea899d58812817b36d92291b5693dd0eeb3af9f91f0f + zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH 4.0.3 diff --git a/pe-client.gemspec b/pe-client.gemspec index ea32c2d..260645c 100644 --- a/pe-client.gemspec +++ b/pe-client.gemspec @@ -43,5 +43,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec-github", "~> 3.0" spec.add_development_dependency "standard", "~> 1.52" spec.add_development_dependency "webmock", "~> 3.26" - spec.add_development_dependency "yard-lint", "~> 1.2" + spec.add_development_dependency "yard-lint", "~> 1.4" end From 827f63788c5df0f9431ca43bb4c249591bad38a2 Mon Sep 17 00:00:00 2001 From: Zach Bensley Date: Fri, 20 Feb 2026 13:02:16 +0800 Subject: [PATCH 5/5] fix: attempt to fix gemlock --- Gemfile.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb2b5fc..5662d58 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,6 @@ PATH specs: pe_client (0.1.0) faraday (~> 2.14) - faraday-multipart (~> 1.2) GEM remote: https://rubygems.org/ @@ -22,8 +21,6 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger - faraday-multipart (1.2.0) - multipart-post (~> 2.0) faraday-net_http (3.4.2) net-http (~> 0.5) hashdiff (1.2.1) @@ -37,7 +34,6 @@ GEM language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) - multipart-post (2.4.1) net-http (0.9.1) uri (>= 0.11.1) parallel (1.27.0) @@ -152,7 +148,6 @@ CHECKSUMS diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c - faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc @@ -161,7 +156,6 @@ CHECKSUMS language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357