├── doc └── intro.md ├── test └── clj_puppetdb │ ├── testutils │ └── repl.clj │ ├── http_core_test.clj │ ├── query_test.clj │ ├── core_test.clj │ ├── http_test.clj │ └── vcr_test.clj ├── README.md ├── .gitignore ├── MAINTAINERS ├── src └── clj_puppetdb │ ├── schema.clj │ ├── http_core.clj │ ├── query.clj │ ├── paging.clj │ ├── core.clj │ ├── vcr.clj │ └── http.clj ├── project.clj ├── dev-resources └── certs │ ├── cert.pem │ ├── ca-cert.pem │ └── key.pem ├── CONTRIBUTING.md └── LICENSE /doc/intro.md: -------------------------------------------------------------------------------- 1 | See README.md for documentation! 2 | -------------------------------------------------------------------------------- /test/clj_puppetdb/testutils/repl.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.testutils.repl 2 | (:require [clojure.tools.namespace.repl :refer [refresh]])) 3 | 4 | (defn reset [] 5 | (refresh)) 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # clj-puppetdb is no longer under active development 3 | 4 | For usage examples see: https://github.com/puppetlabs/clj-puppetdb/blob/master/test/clj_puppetdb/core_test.clj 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /classes/ 3 | /checkouts/ 4 | clj_puppetdb.iml 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.idea/ 10 | /.lein-* 11 | /.nrepl-port 12 | .*.swo 13 | .*.swp 14 | .*.un~ 15 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "file_format": "This MAINTAINERS file format is described at https://github.com/puppetlabs/maintainers", 4 | "issues": "https://github.com/puppetlabs/clj-puppetdb/issues", 5 | "internal_list": "https://groups.google.com/a/puppet.com/forum/?hl=en#!forum/discuss-console-ui-maintainers", 6 | "people": [ 7 | { 8 | "github": "dmcpl", 9 | "email": "david.mccauley@puppet.com", 10 | "name": "David McCauley" 11 | }, 12 | { 13 | "github": "kbrezina", 14 | "email": "karel.brezina@puppet.com", 15 | "name": "Karel Brezina" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/clj_puppetdb/http_core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.http-core-test 2 | (:require [clojure.test :refer :all] 3 | [clj-puppetdb.http-core :as http-core]) 4 | (:import (java.nio.charset Charset))) 5 | 6 | (deftest response-charset-test 7 | (testing "Response contains charset" 8 | (let [charset (Charset/forName "US-ASCII")] 9 | (is (= (http-core/response-charset {:body "Lorem ipsum" :content-type {:charset charset}}) 10 | charset)))) 11 | (testing "Response doesn't contain charset" 12 | (is (= (http-core/response-charset {:body "Lorem ipsum"}) 13 | http-core/default-charset)))) 14 | -------------------------------------------------------------------------------- /src/clj_puppetdb/schema.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.schema 2 | (:require [schema.core :refer [Any Str Int Keyword] :as s]) 3 | (:import [clojure.lang PersistentVector])) 4 | 5 | (def PagingParams 6 | "Schema for params passed to lazy-query." 7 | {(s/required-key :limit) Int 8 | (s/optional-key :offset) Int 9 | (s/required-key :order-by) [{:field (s/either Keyword Str) :order Str}] 10 | (s/optional-key :query) PersistentVector}) 11 | 12 | (def GetParams 13 | "Params ready to be passed to GET. Similar to PagingParams, except 14 | that everything is optional and :query (if present) must be a string." 15 | {(s/optional-key :limit) Int 16 | (s/optional-key :offset) Int 17 | (s/optional-key :order-by) [{:field (s/either Keyword Str) :order Str}] 18 | (s/optional-key :query) Str}) 19 | -------------------------------------------------------------------------------- /src/clj_puppetdb/http_core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.http-core 2 | (:import [java.nio.charset Charset] 3 | [java.io BufferedReader InputStreamReader InputStream])) 4 | 5 | (def default-charset (Charset/forName "UTF-8")) 6 | 7 | (defprotocol PdbClient 8 | "PDB API low level HTTP client protocol." 9 | (pdb-get [this path params] [this that path params] 10 | "Build the query URL and submit the PDB query.") 11 | 12 | (pdb-do-get [this query] 13 | "Do submit the PDB query.") 14 | 15 | (client-info [this] 16 | "Get PDB client info map.")) 17 | 18 | (defn response-charset 19 | "Get the charset to use for decoding of the response body." 20 | ^Charset [response] 21 | (if-let [charset (get-in response [:content-type :charset])] 22 | charset 23 | default-charset)) 24 | 25 | (defn make-response-reader 26 | "Create a buffered reader for reading the response body." 27 | [response] 28 | (BufferedReader. (InputStreamReader. ^InputStream (:body response) (response-charset response)))) 29 | -------------------------------------------------------------------------------- /test/clj_puppetdb/query_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.query-test 2 | (:require [clojure.test :refer :all] 3 | [clj-puppetdb.query :refer [canonicalize-query params->json]])) 4 | 5 | (deftest canonicalize-query-test 6 | (testing "The dreaded ~ operator" 7 | (is (= (canonicalize-query [:match :certname #"web\d+"]) 8 | (canonicalize-query ["~" :certname #"web\d+"]) 9 | ["~" :certname "web\\d+"]))) 10 | (testing "Nested expressions" 11 | (is (= (canonicalize-query [:>= [:fact "uptime_days"] 10]) 12 | [:>= [:fact "uptime_days"] 10]))) 13 | (testing "Booleans don't get converted to strings" 14 | (is (= (canonicalize-query [:= [:fact "is_virtual"] false]) 15 | [:= [:fact "is_virtual"] false])))) 16 | 17 | (deftest params->json-test 18 | (testing "Direct and encoded parameters" 19 | (is (= (params->json {:limit 20 :offset 200 :order_by [{:field :receive_time :order "desc"}]}) 20 | {:limit 20, :offset 200, :order_by "[{\"field\":\"receive_time\",\"order\":\"desc\"}]"})))) 21 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject puppetlabs/clj-puppetdb "0.2.6-SNAPSHOT" 2 | :description "A Clojure client for the PuppetDB REST API" 3 | :url "https://github.com/puppetlabs/clj-puppetdb" 4 | :license {:name "Apache License, Version 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | ;; Abort when version ranges or version conflicts are detected in 7 | ;; dependencies. Also supports :warn to simply emit warnings. 8 | ;; requires lein 2.2.0+. 9 | :pedantic? :abort 10 | :dependencies [[org.clojure/clojure "1.6.0"] 11 | [cheshire "5.4.0"] 12 | [com.cemerick/url "0.1.1"] 13 | [me.raynes/fs "1.4.5"] 14 | [puppetlabs/http-client "0.4.5"] 15 | [prismatic/schema "0.4.0"] 16 | [puppetlabs/kitchensink "1.0.0"] 17 | [slingshot "0.12.2"]] 18 | :plugins [[lein-release "1.0.5"]] 19 | :lein-release {:scm :git 20 | :deploy-via :lein-deploy} 21 | :deploy-repositories [["releases" {:url "https://clojars.org/repo" 22 | :username :env/clojars_jenkins_username 23 | :password :env/clojars_jenkins_password 24 | :sign-releases false}]] 25 | :repl-options {:init (do (require 'spyscope.core) 26 | (use 'clj-puppetdb.testutils.repl))} 27 | :profiles {:dev {:dependencies [[org.clojure/tools.namespace "0.2.5"] 28 | [spyscope "0.1.5" :exclusions [clj-time]]]}}) 29 | -------------------------------------------------------------------------------- /src/clj_puppetdb/query.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.query 2 | (:require [clojure.walk :refer [postwalk]] 3 | [cheshire.core :as json]) 4 | (:import [java.util.regex Pattern])) 5 | 6 | (def ops 7 | {:match "~" 8 | :nil? "null?"}) 9 | 10 | (defn- operator? 11 | [x] 12 | (contains? ops x)) 13 | 14 | (defn- query-walk 15 | "This does most of the hard work for generating a query. 16 | It's intended to be applied recursively to each and every 17 | element of an expression to ensure that the final result 18 | can be converted straightforwardly into JSON. Just about 19 | anything that can be turned into a string will be, but 20 | booleans and numbers pass through unchanged." 21 | [x] 22 | (cond 23 | (operator? x) (ops x) 24 | (instance? Pattern x) (str x) 25 | :else x)) 26 | 27 | (defn canonicalize-query 28 | "Takes a vector approximating an API query (may include some conveniences 29 | like Clojure regex literals and the :match keyword) and converts it into 30 | a form suitable for the API." 31 | [q] 32 | (postwalk query-walk q)) 33 | 34 | (def json-params 35 | "Parameters requiring JSON encoding." 36 | ; TODO remove :order-by and :counts-filter when we drop support for PDB API older than v4 37 | [:query :order_by :counts_filter :order-by :counts-filter]) 38 | 39 | (defn params->json 40 | "Takes a map of PDB request parameters and encodes those parameters which 41 | require it into JSON." 42 | [params] 43 | (reduce 44 | (fn [params key] 45 | (if (contains? params key) 46 | (let [value (get params key)] 47 | ; if the value is a string then we assume it is already JSON encoded 48 | (if (string? value) 49 | params 50 | (->> value 51 | json/encode 52 | (assoc params key)))) 53 | params)) 54 | params 55 | json-params)) 56 | -------------------------------------------------------------------------------- /dev-resources/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF2jCCA8KgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMVQwUgYDVQQDDEtQdXBw 3 | ZXQgQ0EgZ2VuZXJhdGVkIG9uIG1hc3Rlci52YWdyYW50LmludGVybmFsIGF0IDIw 4 | MTUtMDMtMjAgMTQ6NTM6MzEgKzAwMDAwHhcNMTUwMzE5MTQ1MzM0WhcNMjAwMzE4 5 | MTQ1MzM0WjAiMSAwHgYDVQQDDBdtYXN0ZXIudmFncmFudC5pbnRlcm5hbDCCAiIw 6 | DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKOMpJL5TR1Tli0aghQTRSSGZwos 7 | XdYsWjAl5h08I1PChg/mzcXWdWAQ9FgWwAgo9anT3mqUdurmBAfSu/0zc+G80gA4 8 | YHEZOSYhdFm2azO2m77Zu3UU8t4ydH17PFXTo2g06PLyTOX/2Crfi6o7fVys4FO0 9 | j7OFqME2rMnGJ4jzqm8J6ZFMjY31IOfNhFLULumEVVzGlfsz7uyOPzIMLGPIG3+4 10 | zpyQnXCAsU599l4XAqHNCAEpa9J/c7/PoLrfrn3/++tUhSp6FW/PZzjXp17+FX/c 11 | UWxPETs54yZcf0IrGRLmLSIU2I4Rka4Be3bh1hc9Wrkkl9o3v+ttoUO4Cj9VAVGb 12 | LudbPvlsghrubELV5m1DNjg8DJeB4uo9ZpVvR6GhhE5zbb9cSdUA7Yjfuyya90q/ 13 | IufnnmW5QZDrY9mrfFqJl22lQ5N7Ot09A9Cf4vFyrceA9YUCMZir6VqvXW11ievp 14 | LykCHl7jx41AAXSOOzwodok84Zar+IDCh/ykTxbS+s6zxp5Tg9ykfu/tkQwjzbzU 15 | LgLIXOL3YYe3rF+d0Qx647ERXywrIIs83HT5wol+7ZFvMMMu6DtBuKji0TFLUtni 16 | ZiiEOl9R6j7dpExz6fJHMsGbWplBImfnIHsyaSPT64qxRjCRVBGb7LYou+gqL7eB 17 | 7nT0jJ2PVAoTCW0JAgMBAAGjgeYwgeMwNQYJYIZIAYb4QgENBChQdXBwZXQgUnVi 18 | eS9PcGVuU1NMIEludGVybmFsIENlcnRpZmljYXRlMCoGA1UdEQQjMCGCF21hc3Rl 19 | ci52YWdyYW50LmludGVybmFsggZwdXBwZXQwDgYDVR0PAQH/BAQDAgWgMCAGA1Ud 20 | JQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1Ud 21 | DgQWBBQwjMmG0D1SmEdgo5xa1urUeXRCKTAfBgNVHSMEGDAWgBSYg1xYNWfxV+W8 22 | iXQBBIuaRwR1ojANBgkqhkiG9w0BAQsFAAOCAgEARiJcifzWLeETByTYkzOcMqKN 23 | CO1iLaan4hAiLBpKRT7ionBVF0e7AeciNS8ZR9R55YAEq5ZsUHTReLyc9z1HllP1 24 | oQFWeMvWT4waQxXZMix7J0Bym+6voT94AMi6Ga8dMHZyYj5v9RPst3wK1DTHokCv 25 | E6WHo9qeR6T5A2ZC5/P1psHV/nJECk09VjriiHfBfZ4yPNdNR2AJtQhcArTieUlY 26 | G+Buf3HuxKCviV1qjbxY8M53maj1ilpPoLHZaoOeW2nWes5kvkzzMjWKJ3qyqzXi 27 | HWatX6lIrU/2oG5xc6Vg95rNJAC49pqegu2x0hxsFqW7N+Nbu4Xah05+TfE4I6Ey 28 | dKp7SAsfiOOVr/gYvYVKy4yV87zHUZbKYQ8+38dJJMQlndQY3g+CT5RhfmCjlsf0 29 | naqOQ3rFxi/CX96wzkwcUdLcxIdJXuf76VdAQZ5w9AGG6uG1sA/7eItjatNmjTtg 30 | femxuvc3t6+tDfKZv+a/6e0Iqy4sRA4kHG4GWpju0w/900q4r1ikZC/dg4O8W43B 31 | 1AlVYv0YiJcuBj1hNP7IpIvZG9LiL1/4k+RzQSa2LDfgKLNzxUYNtxHea3izErOQ 32 | yjcMcHttZm72WH+u0hpxe5XdHxPe/6Fqh6PrX9K7xybFchM74Fce16JCmJqGZmhf 33 | qhCeUOdVi1JyxWGbkQY= 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /dev-resources/certs/ca-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGDDCCA/SgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMVQwUgYDVQQDDEtQdXBw 3 | ZXQgQ0EgZ2VuZXJhdGVkIG9uIG1hc3Rlci52YWdyYW50LmludGVybmFsIGF0IDIw 4 | MTUtMDMtMjAgMTQ6NTM6MzEgKzAwMDAwHhcNMTUwMzE5MTQ1MzMzWhcNMjAwMzE4 5 | MTQ1MzMzWjBWMVQwUgYDVQQDDEtQdXBwZXQgQ0EgZ2VuZXJhdGVkIG9uIG1hc3Rl 6 | ci52YWdyYW50LmludGVybmFsIGF0IDIwMTUtMDMtMjAgMTQ6NTM6MzEgKzAwMDAw 7 | ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDD1dws3IBfyk6kxEgp6IhH 8 | NZGFa6YAKk55dj9pUOWpndFXAJFSdnWKsn3qpoEGg9TBhn/Ez/CdhLMZqmupk2Uo 9 | nUBUuLff3czEHQONMQdvVXnmLEBuesoFMBvLQVyMJzROnKTWLyAXutZT41dVocc0 10 | NMtq0SKE/rKp8342t/u675f7AJJaQJrRg3hc4Ry3Wh7RFNRFSoSUeaXtoM9V8Hyf 11 | UBAuYko6P3sMDk2zUj4bUbjA4et/hWy8wocdxL+b5Bf5VSe60EpZNaDdHQSm5+1F 12 | ykL7JE/wgtcuqXzk7NOpfSx8dJuNjt2LyXOr8zLWDkNBzBOtG/0sUEerIBeuWb8+ 13 | 74C1e7GsbcDvjQRC9UCcBJ+krlWFMYLG2ylcxj6W1IHSk16LkN91oRQ3TfcWcbvd 14 | ekj/M84QoPXhco8t1CE7R7cMxkB1UtdV+gBF59nds0prI84V6nKH4AuyL4iBopQw 15 | TL35rpQ7QgH54vrTmAt/Emfa5xmyfRfHbqJpcswkyfwYP6/zoX83KFxj6LUNclFy 16 | J4jwwTRwtvPbHpQaRCxlnQXHklUL5oOAmJCbyt95H/8CjSZk93P4lPJ5fj+01mWp 17 | w3+EmvYT56TjPqnTcicBVRxO02OyCpbQKpiUZaayaFhBwBUlszXKXmaijfwqZr3T 18 | obdNVNP1Pfw0s127hdbzQwIDAQABo4HkMIHhMDUGCWCGSAGG+EIBDQQoUHVwcGV0 19 | IFJ1YnkvT3BlblNTTCBJbnRlcm5hbCBDZXJ0aWZpY2F0ZTAOBgNVHQ8BAf8EBAMC 20 | AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUmINcWDVn8VflvIl0AQSLmkcE 21 | daIwaAYDVR0jBGEwX6FapFgwVjFUMFIGA1UEAwxLUHVwcGV0IENBIGdlbmVyYXRl 22 | ZCBvbiBtYXN0ZXIudmFncmFudC5pbnRlcm5hbCBhdCAyMDE1LTAzLTIwIDE0OjUz 23 | OjMxICswMDAwggEBMA0GCSqGSIb3DQEBCwUAA4ICAQBOA2fdaYfwfQHlt3oOd3kD 24 | JdVo0n8tDr3UPzCvjf5g8lPWihzAeXDgNFDbF3N7Cwl/9NwoFb1j73IEBnHw1N3x 25 | E777UP9BUNxMSYsJo2SoGAD5OHBiV9knzlPQhnFzmakQE+ubslULWn/+iwjERicP 26 | 1QJd3dziNFs2R/CPGATORcaufQWuSpdvZnBKXAmzUK93nWz4K8sNbh9dzuP+vRWy 27 | 6j6v8zKas+F4Bkq6aNfoFaHTkfO+mrhRcF1Bop8NVSVeg0s2ybjw1dImn0CqAzML 28 | CKr1BqmKP+422cab2og/No0PuLrG4xf+YPlbUGynPa+VL3oSIUJgAC9eQ2x0XrNF 29 | th0Z2Qj5nFNrsUar4nfMXXHge/SNKajl47jcWi2BS2i5416yHxMB75Q/h02rCpBz 30 | SaPqPhF+1MSkwHNZ29uICaTCrOmyc/d6BGUnIzPAWiG1E6sGCugqMNFZ0zSyc8uG 31 | qjeWCzZ5o4eTIXxrvR2CBI9FGIjvcDi68TNbQYlqfF1x63ZzRpsdm2s93E/Sk2xX 32 | E9pfu2SCpQFGNGGGw4ndpMCAugUpw+zsh1Q/Ak/xXvcxUGnFiujm5qJoSZc1gI9u 33 | HG1uHVXraVf2pZfrt81d8iVQj+Ivhhkb0/0EblDhSNsYrhpXY7PH7+XR86L+SZDh 34 | e+hXIkJ52BcFW4BfhawjYg== 35 | -----END CERTIFICATE----- 36 | -------------------------------------------------------------------------------- /src/clj_puppetdb/paging.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.paging 2 | (:require [clj-puppetdb.schema :refer [PagingParams]] 3 | [clj-puppetdb.http :refer [GET]] 4 | [clj-puppetdb.query :as q] 5 | [schema.core :as s])) 6 | 7 | (defn- refresh 8 | "Given a results map from a paging query, request and return the 9 | map with the next set of results, or return nil if there are no 10 | more results. Never returns a map with an empty :body." 11 | [{:keys [limit offset query]}] 12 | (when-let [new-body (query offset)] 13 | {:body new-body 14 | :limit limit 15 | :offset (+ limit offset) 16 | :query (if (= limit (count new-body)) ;; if we hit the limit 17 | query ;; put the query back, 18 | ;; otherwise return a dummy query fn 19 | (constantly nil))})) 20 | 21 | (defn- ensure-refreshed 22 | "Given a results map, refreshes it if necessary. Otherwise, return 23 | the results unchanged. Never returns a map with an empty :body; 24 | The return value will be nil if the results have been exhausted." 25 | [results] 26 | (if (empty? (:body results)) 27 | (refresh results) 28 | results)) 29 | 30 | (defn- lazy-page 31 | "Return a lazy sequence of results from the given results map, 32 | requesting further results from the PuppetDB server as needed." 33 | [results] 34 | (lazy-seq 35 | (when-let [refreshed (ensure-refreshed results)] 36 | (cons 37 | (first (:body refreshed)) 38 | (lazy-page (update-in refreshed [:body] rest)))))) 39 | 40 | (defn- lazify-query 41 | "Returns a map containing initial results with enough contextual data 42 | to request the next set of results." 43 | ([client path params] 44 | (let [limit (get params :limit) 45 | offset (get params :offset 0) 46 | query-fn (fn [offset] 47 | (GET client path (assoc params :offset offset)))] 48 | {:body nil 49 | :limit limit 50 | :offset offset 51 | :query query-fn})) 52 | ([client path query-vec params] 53 | (let [params-with-query (assoc params :query (q/canonicalize-query query-vec))] 54 | (lazify-query client path params-with-query)))) 55 | 56 | (s/defn ^:always-validate lazy-query 57 | "Return a lazy sequence of results from the given query. Unlike the regular 58 | `query` function, `lazy-query` uses paging to fetch results gradually as they 59 | are consumed. 60 | 61 | The `params` map is required, and should contain the following keys: 62 | - :limit (the number of results to request) 63 | - :offset (optional: the index of the first result to return, default 0) 64 | - :order-by (a vector of maps, each specifying a :field and an :order key) 65 | For example: `{:limit 100 :offset 0 :order-by [{:field \"value\" :order \"asc\"}]}`" 66 | ([client path params :- PagingParams] 67 | (-> (lazify-query client path params) 68 | lazy-page)) 69 | ([client path query-vec params :- PagingParams] 70 | (-> (lazify-query client path query-vec params) 71 | lazy-page))) 72 | -------------------------------------------------------------------------------- /dev-resources/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /src/clj_puppetdb/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.core 2 | (:require [clj-puppetdb.http :refer [GET] :as http] 3 | [clj-puppetdb.paging :as paging] 4 | [clj-puppetdb.query :as q] 5 | [schema.core :as s] 6 | [puppetlabs.http.client.common :as http-common]) 7 | (:import (clj_puppetdb.http_core PdbClient))) 8 | 9 | (s/defn connect :- PdbClient 10 | "Return a PuppetDB client map for the given host. 11 | 12 | If the host begins with 'https://', you must supply also supply a map containing 13 | a valid :ssl-ca-cert, :ssl-cert, and :ssl-key (as java.io.File objects). 14 | 15 | If the host begins with 'http://', that must be the only argument given. 16 | 17 | Either way, you must specify the port as part of the host URL (usually ':8080' for 18 | http or ':8081' for https)." 19 | ([http-async-client :- (s/protocol http-common/HTTPClient) 20 | host :- s/Str] 21 | (connect http-async-client host {})) 22 | ([http-async-client :- (s/protocol http-common/HTTPClient) 23 | host :- s/Str 24 | opts] 25 | (http/make-client http-async-client host opts))) 26 | 27 | (defn- query-pdb 28 | [client path query-vec params] 29 | (let [params (if params params {}) 30 | merged-params (if query-vec 31 | (assoc params :query (q/canonicalize-query query-vec)) 32 | params) 33 | [body headers] (GET client path merged-params) 34 | total (get headers "x-records") 35 | metadata (try (if total {:total (BigInteger. ^String total)}) (catch Throwable _))] 36 | [body metadata])) 37 | 38 | (defn query-with-metadata 39 | "Use the given PuppetDB client to query the server. 40 | 41 | The path argument should be a valid endpoint, e.g. \"/v4/nodes\". 42 | 43 | The query-vec argument is optional, and should be a vector representing an API query, 44 | e.g. [:= [:fact \"operatingsystem\"] \"Linux\"] 45 | 46 | The params map is optional, and can contain any of the following keys: 47 | - :order-by (a vector of maps, each specifying a :field and an :order key) 48 | - :limit (the number of results to request) 49 | - :offset (the index of the first result to return, defaults to 0) 50 | - :include-total (boolean indicating whether to return the total number of records available) 51 | For example: `{:limit 100 :offset 0 :order-by [{:field \"value\" :order \"asc\"}] :include-total true}` 52 | 53 | This function returns two maps in a vector. The first map is the query result as returned by 'query'. The second 54 | contains additional metadata. Currently the only supported kind of metadata is: 55 | - :total (the total number of records available)" 56 | ([client path params] 57 | (query-with-metadata client path nil params)) 58 | ([client path query-vec params] 59 | (query-pdb client (str "/pdb/query" path) query-vec params))) 60 | 61 | (defn query-ext-with-metadata 62 | "Same semantics as query-with-metadata but used to access PE extensions to PuppetDBq" 63 | ([client path params] 64 | (query-ext-with-metadata client path nil params)) 65 | ([client path query-vec params] 66 | (query-pdb client (str "/pdb/ext" path) query-vec params))) 67 | 68 | (defn query 69 | "Use the given PuppetDB client to query the server. 70 | 71 | The path argument should be a valid endpoint, e.g. \"/v4/nodes\". 72 | 73 | The query-vec argument is optional, and should be a vector representing an API query, 74 | e.g. [:= [:fact \"operatingsystem\"] \"Linux\"]" 75 | ([client path] 76 | (first (query-with-metadata client path nil))) 77 | ([client path query-vec] 78 | (first (query-with-metadata client path query-vec nil)))) 79 | 80 | (defn lazy-query 81 | "Return a lazy sequence of results from the given query. Unlike the regular 82 | `query` function, `lazy-query` uses paging to fetch results gradually as they 83 | are consumed. 84 | 85 | The `params` map is required, and should contain the following keys: 86 | - :limit (the number of results to request) 87 | - :offset (optional: the index of the first result to return, default 0) 88 | - :order-by (a vector of maps, each specifying a :field and an :order key) 89 | For example: `{:limit 100 :offset 0 :order-by [{:field \"value\" :order \"asc\"}]}`" 90 | ([client path params] 91 | (paging/lazy-query client path params)) 92 | ([client path query-vec params] 93 | (paging/lazy-query client path query-vec params))) 94 | -------------------------------------------------------------------------------- /test/clj_puppetdb/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.core-test 2 | (:require [clojure.test :refer :all] 3 | [clj-puppetdb.core :refer :all] 4 | [clj-puppetdb.http-core :refer :all] 5 | [clj-puppetdb.http :refer [GET]] 6 | [puppetlabs.http.client.sync :as http] 7 | [puppetlabs.http.client.async :as async])) 8 | 9 | (deftest connect-test 10 | (let [http-client (async/create-client {})] 11 | (testing "Should create a connection with http://localhost:8080 with no additional arguments" 12 | (let [conn (connect http-client "http://localhost:8080")] 13 | (is (= (client-info conn) {:host "http://localhost:8080"})))) 14 | (testing "Should create a connection with http://localhost:8080 and VCR enabled" 15 | (let [conn (connect http-client "http://localhost:8080" {:vcr-dir "/temp"})] 16 | (is (= (client-info conn) {:host "http://localhost:8080" :vcr-dir "/temp"})))) 17 | (testing "Should accept https://puppetdb:8081 with no opts" 18 | (let [conn (connect http-client "https://puppetdb:8081" {})] 19 | ;; I'm only testing for truthiness of conn here. Schema validation should handle the rest, 20 | ;; and testing equality with java.io.File objects doesn't seem to work. 21 | (is conn))) 22 | (testing "Should accept VCR enabled" 23 | (let [opts {:vcr-dir "/temp"} 24 | conn (connect http-client "https://puppetdb:8081" opts)] 25 | ;; I'm testing for truthiness of conn here. Schema validation should handle the rest except the VCR piece, 26 | ;; and testing equality with java.io.File objects doesn't seem to work. 27 | (is conn) 28 | (is (= (:vcr-dir opts) (:vcr-dir (client-info conn)))))) 29 | (testing "Should not accept bad host" 30 | (is (thrown? AssertionError (connect http-client "httpXXX://puppetdb:8081" {})))) 31 | (testing "Should do proper GET request" 32 | (let [host "http://localhost:8080" 33 | path "/v4/nodes" 34 | params {:query [:= [:fact "operatingsystem"] "Linux"]} 35 | url-params "?query=%5B%22%3D%22%2C%5B%22fact%22%2C%22operatingsystem%22%5D%2C%22Linux%22%5D" 36 | conn (connect http-client host)] 37 | (with-redefs [http/get (fn [query _] (is (= query (str host path url-params))) [])] 38 | (pdb-get conn path params) 39 | (pdb-do-get conn (str host path url-params))))))) 40 | 41 | (deftest query-test 42 | (let [data ["node-01" "node-02" "node-03" "node-04"] 43 | metadata {:total (count data)} 44 | client "a client" 45 | path "/v4/nodes"] 46 | (testing "Should return data when parameters are not used" 47 | (with-redefs [GET (fn[_ _ _] 48 | [data {"x-records" (.toString (count data))}])] 49 | (is (= (query client path) 50 | data)) 51 | (is (= (query-with-metadata client path {}) 52 | [data metadata])))) 53 | (testing "Should return data when parameters are used" 54 | (let [query-vec [:= [:fact "operatingsystem"] "Linux"]] 55 | (with-redefs [GET (fn[_ _ params] 56 | (is (= (:query params) query-vec)) 57 | [data {"x-records" (.toString (count data))}])] 58 | (is (= (query client path query-vec) 59 | data)) 60 | (is (= (query-with-metadata client path query-vec {}) 61 | [data metadata])) 62 | (is (= (query-with-metadata client path {:query query-vec}) 63 | [data metadata]))))))) 64 | 65 | (deftest lazy-query-test 66 | (testing "Should automatically fetch three pages by two items" 67 | (let [data [["A" "B"] ["C" "D"] ["E" "F"]]] 68 | (with-redefs [clj-puppetdb.http/GET (fn[_ _ {:keys [offset]}] 69 | (let [idx (/ offset 2)] 70 | (when (< idx 3) (nth data idx))))] 71 | (let [seq (lazy-query nil nil {:limit 2 :order-by nil})] 72 | (is (= seq (flatten data))))))) 73 | (testing "Should automatically fetch two pages by tree items" 74 | (let [data [["A" "B" "C"] [ "D" "E" "F"]]] 75 | (with-redefs [clj-puppetdb.http/GET (fn[_ _ {:keys [offset]}] 76 | (let [idx (/ offset 3)] 77 | (when (< idx 2) (nth data idx))))] 78 | (let [seq (lazy-query nil nil {:limit 3 :order-by nil})] 79 | (is (= seq (flatten data)))))))) 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Third-party patches are essential for keeping puppet great. There are a few guidelines that we 4 | need contributors to follow so that we can have a chance of keeping on top of things. 5 | 6 | ## Getting Started 7 | 8 | * Make sure you have a [Jira account](http://tickets.puppetlabs.com) 9 | * Make sure you have a [GitHub account](https://github.com/signup/free) 10 | * Submit a ticket for your issue, assuming one does not already exist. 11 | * Clearly describe the issue including steps to reproduce when it is a bug. 12 | * Make sure you fill in the earliest version that you know has the issue. 13 | * Fork the repository on GitHub 14 | 15 | ## Making Changes 16 | 17 | * Create a topic branch from where you want to base your work. 18 | * This is usually the master branch. 19 | * Only target release branches if you are certain your fix must be on that 20 | branch. 21 | * To quickly create a topic branch based on master; `git checkout -b 22 | fix/master/my_contribution master`. Please avoid working directly on the 23 | `master` branch. 24 | * Make commits of logical units. 25 | * Check for unnecessary whitespace with `git diff --check` before committing. 26 | * Make sure your commit messages are in the proper format. 27 | 28 | ```` 29 | (PUP-1234) Make the example in CONTRIBUTING imperative and concrete 30 | 31 | Without this patch applied the example commit message in the CONTRIBUTING 32 | document is not a concrete example. This is a problem because the 33 | contributor is left to imagine what the commit message should look like 34 | based on a description rather than an example. This patch fixes the 35 | problem by making the example concrete and imperative. 36 | 37 | The first line is a real life imperative statement with a ticket number 38 | from our issue tracker. The body describes the behavior without the patch, 39 | why this is a problem, and how the patch fixes the problem when applied. 40 | ```` 41 | 42 | * Make sure you have added the necessary tests for your changes. 43 | * Run _all_ the tests to assure nothing else was accidentally broken. 44 | 45 | ## Making Trivial Changes 46 | 47 | ### Documentation 48 | 49 | For changes of a trivial nature to comments and documentation, it is not 50 | always necessary to create a new ticket in Jira. In this case, it is 51 | appropriate to start the first line of a commit with '(doc)' instead of 52 | a ticket number. 53 | 54 | ```` 55 | (doc) Add documentation commit example to CONTRIBUTING 56 | 57 | There is no example for contributing a documentation commit 58 | to the Puppet repository. This is a problem because the contributor 59 | is left to assume how a commit of this nature may appear. 60 | 61 | The first line is a real life imperative statement with '(doc)' in 62 | place of what would have been the ticket number in a 63 | non-documentation related commit. The body describes the nature of 64 | the new documentation or comments added. 65 | ```` 66 | 67 | ## Submitting Changes 68 | 69 | * Sign the [Contributor License Agreement](http://links.puppetlabs.com/cla). 70 | * Push your changes to a topic branch in your fork of the repository. 71 | * Submit a pull request to the repository in the puppetlabs organization. 72 | * Update your Jira ticket to mark that you have submitted code and are ready for it to be reviewed (Status: Ready for Merge). 73 | * Include a link to the pull request in the ticket. 74 | * The core team looks at Pull Requests on a regular basis in a weekly triage 75 | meeting that we hold in a public Google Hangout. The hangout is announced in 76 | the weekly status updates that are sent to the puppet-dev list. Notes are 77 | posted to the [Puppet Community community-triage 78 | repo](https://github.com/puppet-community/community-triage/tree/master/core/notes) 79 | and include a link to a YouTube recording of the hangout. 80 | * After feedback has been given we expect responses within two weeks. After two 81 | weeks we may close the pull request if it isn't showing any activity. 82 | 83 | # Additional Resources 84 | 85 | * [Puppet Labs community guildelines](http://docs.puppetlabs.com/community/community_guidelines.html) 86 | * [Bug tracker (Jira)](http://tickets.puppetlabs.com) 87 | * [Contributor License Agreement](http://links.puppetlabs.com/cla) 88 | * [General GitHub documentation](http://help.github.com/) 89 | * [GitHub pull request documentation](http://help.github.com/send-pull-requests/) 90 | * #puppet-dev IRC channel on freenode.org ([Archive](https://botbot.me/freenode/puppet-dev/)) 91 | * [puppet-dev mailing list](https://groups.google.com/forum/#!forum/puppet-dev) 92 | * [Community PR Triage notes](https://github.com/puppet-community/community-triage/tree/master/core/notes) 93 | -------------------------------------------------------------------------------- /src/clj_puppetdb/vcr.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.vcr 2 | (:require [cheshire.core :as json] 3 | [clj-puppetdb.http-core :refer :all] 4 | [clojure.edn :as edn] 5 | [clojure.java.io :as io] 6 | [me.raynes.fs :as fs] 7 | [puppetlabs.kitchensink.core :refer [utf8-string->sha1]]) 8 | (:import [java.io File ByteArrayInputStream InputStreamReader BufferedReader PushbackReader InputStream] 9 | [java.nio.charset Charset] 10 | [org.apache.http.entity ContentType])) 11 | 12 | (def mock-server "http://pdb-mock-host:0") 13 | 14 | (defn rebuild-content-type 15 | "Rebuild the `:content-type` key in the given `response` map based on the value of the content-type 16 | header." 17 | [response] 18 | (let [content-type-header (get-in response [:headers "content-type"])] 19 | (if (empty? content-type-header) 20 | response 21 | (let [content-type (ContentType/parse content-type-header)] 22 | (assoc response :content-type 23 | {:mime-type (.getMimeType content-type) 24 | :charset (.getCharset content-type)}))))) 25 | 26 | (defn body->string 27 | [response] 28 | "Turn response body into a string." 29 | (let [charset (response-charset response)] 30 | (->> (-> ^InputStream (get response :body) 31 | (InputStreamReader. ^Charset charset) 32 | BufferedReader. 33 | slurp) 34 | (assoc response :body)))) 35 | 36 | (defn body->stream 37 | [response] 38 | "Turn response body into a `java.io.InputStream` subclass." 39 | (let [charset (response-charset response)] 40 | (->> (-> ^String (get response :body) 41 | (.getBytes ^Charset charset) 42 | ByteArrayInputStream.) 43 | (assoc response :body)))) 44 | 45 | (defn- vcr-file 46 | [vcr-dir query] 47 | (fs/file (File. (str vcr-dir "/" (utf8-string->sha1 query) ".clj")))) 48 | 49 | (defn- vcr-serialization-transform 50 | "Prepare the response for searialization." 51 | [response mock-query] 52 | (-> response 53 | body->string 54 | (dissoc :content-type) 55 | (assoc-in [:opts :url] mock-query))) 56 | 57 | (defn- vcr-unserialization-transform 58 | "Rebuild the response after unseralization." 59 | [response query] 60 | (-> response 61 | (assoc-in [:opts :url] query) 62 | rebuild-content-type 63 | body->stream)) 64 | 65 | (def nested-params 66 | "parameters which contain nested maps" 67 | ; TODO remove :order-by when we drop support for PDB API older than v4 68 | [:order_by :order-by]) 69 | 70 | (defn- normalize-params 71 | "certain parmas (notably order_by) contain nested maps and if the VCR is running we want to 72 | enforce a specific ordering to give us URL stability" 73 | [params] 74 | (reduce 75 | (fn [params key] 76 | (if (contains? params key) 77 | (let [value (get params key)] 78 | (->> 79 | ; if the value is a string then we assume it is JSON encoded in which case we 80 | ; need to decode it first 81 | (if (string? value) 82 | (json/decode value) 83 | value) 84 | (map #(into (sorted-map) %)) 85 | (assoc params key))) 86 | params)) 87 | params 88 | nested-params)) 89 | 90 | (defn make-vcr-client 91 | "Make VCR-enabled version of the supplied `client` that will check for a file containing 92 | a response first. If none is found the original client is called to obtain the response, 93 | which is then recorded for the future." 94 | [vcr-dir client] 95 | (let [prefix-length (-> client client-info :host count)] 96 | (reify 97 | PdbClient 98 | (pdb-get [this path params] 99 | (pdb-get this this path params)) 100 | (pdb-get [_ that path params] 101 | ; Sort the known nested structures in the query parameters to give us URL stability and 102 | ; then delegate to the original client. 103 | (->> params 104 | normalize-params 105 | (pdb-get client that path))) 106 | 107 | (pdb-do-get [_ query] 108 | (let [mock-query (str mock-server (subs query prefix-length)) 109 | file (vcr-file vcr-dir mock-query)] 110 | (when-not (fs/exists? file) 111 | (let [response (-> (pdb-do-get client query) 112 | (vcr-serialization-transform mock-query))] 113 | (fs/mkdirs (fs/parent file)) 114 | (-> file 115 | io/writer 116 | (spit response)))) 117 | ; Always read from the file - even if we just wrote it - to fast-fail on serialization errors 118 | ; (at the expense of performance) 119 | (-> (with-open [reader (-> file 120 | io/reader 121 | PushbackReader.)] 122 | (edn/read reader)) 123 | (vcr-unserialization-transform query)))) 124 | 125 | (client-info [_] 126 | (-> client 127 | client-info 128 | (assoc :vcr-dir vcr-dir)))))) 129 | -------------------------------------------------------------------------------- /test/clj_puppetdb/http_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.http-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.io :as io] 4 | [clj-puppetdb.http :refer [GET make-client catching-exceptions assoc-kind]] 5 | [clj-puppetdb.http-core :refer :all] 6 | [cheshire.core :as json] 7 | [puppetlabs.http.client.async :as http] 8 | [slingshot.slingshot :refer [try+]])) 9 | 10 | (defn- test-query-params 11 | [client params assert-fn] 12 | (let [wrapped-client 13 | (reify 14 | PdbClient 15 | (pdb-get [this path params] 16 | (pdb-get this this path params)) 17 | (pdb-get [_ that path params] 18 | (pdb-get client that path params)) 19 | 20 | (pdb-do-get [_ query] 21 | (assert-fn query)) 22 | 23 | (client-info [_] 24 | (client-info client)))] 25 | (pdb-get wrapped-client "" params))) 26 | 27 | (deftest parameter-encoding-test 28 | (let [http-client (http/create-client {})] 29 | (let [client (make-client http-client "http://localhost:8080" {})] 30 | (testing "Should JSON encode parameters which requre it" 31 | (test-query-params client {:foo [:bar "baz"] 32 | :counts_filter [:> "failures" 0] 33 | :query [:= :certname "node"] 34 | :order_by [{:field "status" :order "ASC"}]} 35 | #(is (= % "http://localhost:8080?counts_filter=%5B%22%3E%22%2C%22failures%22%2C0%5D&foo=%5B%3Abar%20%22baz%22%5D&order_by=%5B%7B%22field%22%3A%22status%22%2C%22order%22%3A%22ASC%22%7D%5D&query=%5B%22%3D%22%2C%22certname%22%2C%22node%22%5D")))) 36 | (testing "Should leave already encoded params alone" 37 | (test-query-params client {:order_by "[{\"order\":\"ASC\",\"field\":\"status\"}]"} 38 | #(is (= % "http://localhost:8080?order_by=%5B%7B%22order%22%3A%22ASC%22%2C%22field%22%3A%22status%22%7D%5D"))))) 39 | (let [client (make-client http-client "http://localhost:8080" {:vcr-dir "foo"})] 40 | (testing "Should sort parameters contianing nested structures" 41 | (test-query-params client {:order_by [{:order "ASC" :field "status"}]} 42 | #(is (= % "http://localhost:8080?order_by=%5B%7B%22field%22%3A%22status%22%2C%22order%22%3A%22ASC%22%7D%5D")))) 43 | (testing "Should sort parameters contianing nested structures even if already JSON encoded" 44 | (test-query-params client {:order_by "[{\"order\":\"ASC\",\"field\":\"status\"}]"} 45 | #(is (= % "http://localhost:8080?order_by=%5B%7B%22field%22%3A%22status%22%2C%22order%22%3A%22ASC%22%7D%5D"))))))) 46 | 47 | (deftest GET-test 48 | (let [q-host "http://localhost:8080" 49 | q-path "/v4/nodes" 50 | q-params {:query [:= [:fact "operatingsystem"] "Linux"]} 51 | client (make-client (http/create-client {}) q-host {}) 52 | response-data ["node-1" "node-2"] 53 | response-data-encoded (json/encode response-data) 54 | response-headers {"x-records" (.toString (count response-data))} 55 | fake-get (fn [status] {:status status :body (io/input-stream (.getBytes response-data-encoded)) :headers response-headers})] 56 | 57 | (testing "Should have proper response" 58 | (with-redefs [http/request-with-client (fn [_ _ _] (future (fake-get 200)))] 59 | (let [GET-response (GET client q-path q-params)] 60 | (is (= (first GET-response) response-data)) 61 | (is (= (second GET-response) response-headers))))) 62 | 63 | (testing "Should throw proper exception" 64 | (with-redefs [http/request-with-client (fn [_ _ _] (future (fake-get 400)))] 65 | (try+ 66 | (GET client q-path q-params) 67 | (catch [] {:keys [status kind params endpoint host msg]} 68 | (is (= status 400)) 69 | (is (= kind :puppetdb-query-error)) 70 | (is (= params q-params)) 71 | (is (= endpoint q-path)) 72 | (is (= host q-host)) 73 | (is (= msg response-data-encoded)))))) 74 | 75 | (testing "Should throw proper exception on an error response" 76 | (with-redefs [http/request-with-client (fn [_ _ _] (future ((constantly {:error "an exception"}))))] 77 | (try+ 78 | (GET client q-path q-params) 79 | (catch [] {:keys [kind exception]} 80 | (is (= kind :puppetdb-connection-error)) 81 | (is (= exception "an exception")))))))) 82 | 83 | (deftest catching-exceptions-test 84 | (testing "Should pass" 85 | (is (= (catching-exceptions ((constantly {:body "foobar"})) (assoc-kind {} :something-bad-happened)) {:body "foobar"}))) 86 | 87 | (testing "Should rethrow proper exception on an exception" 88 | (try+ 89 | (catching-exceptions (#(throw (NullPointerException.))) (assoc-kind {} :something-bad-happened)) 90 | (is (not "Should never get to this place!!")) 91 | (catch [] {:keys [kind exception]} 92 | (is (= kind :something-bad-happened)) 93 | (is (instance? NullPointerException exception))))) 94 | 95 | (testing "Should rethrow proper exception on an exception that is listed" 96 | (try+ 97 | (catching-exceptions (#(throw (NullPointerException.))) (assoc-kind {} :something-bad-happened) NullPointerException) 98 | (is (not "Should never get to this place!!")) 99 | (catch [] {:keys [kind exception]} 100 | (is (= kind :something-bad-happened)) 101 | (is (instance? NullPointerException exception))))) 102 | 103 | (testing "Should throw original exception on an exception that is not listed" 104 | (try 105 | (catching-exceptions (#(throw (NullPointerException.))) (assoc-kind {} :something-bad-happened) ArithmeticException) 106 | (is (not "Should never get to this place!!")) 107 | (catch NullPointerException e 108 | (is (instance? NullPointerException e)))))) -------------------------------------------------------------------------------- /src/clj_puppetdb/http.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.http 2 | (:require [cemerick.url :refer [map->query]] 3 | [cheshire.core :as json] 4 | [clj-puppetdb.http-core :refer :all] 5 | [clj-puppetdb.query :as q] 6 | [clj-puppetdb.vcr :refer [make-vcr-client]] 7 | [clojure.tools.logging :as log] 8 | [me.raynes.fs :as fs] 9 | [puppetlabs.http.client.async :as http-async] 10 | [puppetlabs.http.client.common :as http-common] 11 | [puppetlabs.ssl-utils.core :as ssl] 12 | [schema.core :as s] 13 | [slingshot.slingshot :refer [throw+]]) 14 | (:import [java.io IOException File] 15 | [javax.net.ssl SSLContext] 16 | [com.fasterxml.jackson.core JsonParseException])) 17 | 18 | ;; TODO: 19 | ;; - Validate schema for GET params. The GetParams schema 20 | ;; exists, but needs work before it can be used. 21 | 22 | (def cert-keys 23 | "The keys to the configuration map specifying the certificates/private key 24 | needed for creating the SSL context. 25 | Warning: the order of the keys must match that expected by the 26 | `puppetlabs.ssl-utils.core/pems->ssl-context` function." 27 | [:ssl-cert :ssl-key :ssl-ca-cert]) 28 | 29 | (def connection-relevant-opts 30 | [:ssl-context :connect-timeout-milliseconds :socket-timeout-milliseconds]) 31 | 32 | (defn- make-client-common 33 | [http-async-client ^String host opts] 34 | (let [conn-opts (select-keys opts connection-relevant-opts) 35 | req-opts (apply dissoc opts connection-relevant-opts)] 36 | (reify 37 | PdbClient 38 | (pdb-get [this path params] 39 | (pdb-get this this path params)) 40 | ; this arity is meant to support a kind of polymorphism 41 | (pdb-get [_ that path params] 42 | (let [query (if (empty? params) 43 | (str host path) 44 | (str host path \? (-> params 45 | q/params->json 46 | map->query)))] 47 | (pdb-do-get that query))) 48 | 49 | (pdb-do-get [_ query] 50 | (log/debug (str "GET:" query)) 51 | @(http-common/get http-async-client query req-opts)) 52 | 53 | (client-info [_] 54 | (assoc conn-opts :host host))))) 55 | 56 | (defn- file? 57 | [^String file-path] 58 | (if (nil? file-path) 59 | nil 60 | (-> file-path 61 | File. 62 | fs/file?))) 63 | 64 | (defn make-client 65 | [http-async-client ^String host opts] 66 | {:pre [(or (.startsWith host "https://") 67 | (.startsWith host "http://"))] 68 | :post [(satisfies? PdbClient %)]} 69 | (let [vcr-dir (:vcr-dir opts) 70 | opts (dissoc opts :vcr-dir) 71 | client (make-client-common http-async-client host opts)] 72 | (if vcr-dir 73 | (make-vcr-client vcr-dir client) 74 | client))) 75 | 76 | (defmacro assoc-kind 77 | "Associate the supplied `kind` value with the :kind key in the given `exception-structure` map." 78 | [exception-structure kind] 79 | `(assoc ~exception-structure :kind ~kind)) 80 | 81 | (defmacro catching-exceptions 82 | "Execute the `call` in a try-catch block, catching the named `exceptions` (or any subclasses 83 | of `java.lang.Throwable`) and rethrowing them as :exception in the `exception-structure`." 84 | [call exception-structure & exceptions] 85 | (let [exceptions (if (empty? exceptions) [Throwable] exceptions)] 86 | `(try 87 | ~call 88 | ~@(map (fn [exception] 89 | `(catch ~exception exception# 90 | (throw+ (assoc ~exception-structure :exception exception#)))) 91 | exceptions)))) 92 | 93 | (defmacro catching-parse-exceptions 94 | "A convenience macro for wrapping JSON parsing code. It simply delegates to the 95 | `catching-exceptions` macro supplying arguments to it suitable for the JSON parsing." 96 | [call exception-structure] 97 | `(catching-exceptions 98 | ~call 99 | (assoc-kind ~exception-structure :puppetdb-parse-error) JsonParseException IOException)) 100 | 101 | (defn- lazy-seq-catching-parse-exceptions 102 | "Given a lazy sequence wrap it into another lazy sequence which ensures that proper error 103 | handling is in place whenever an element is consumed from the sequence." 104 | [result exception-structure] 105 | (lazy-seq 106 | (if-let [sequence (catching-parse-exceptions (seq result) exception-structure)] 107 | (cons (first sequence) (lazy-seq-catching-parse-exceptions (rest sequence) exception-structure)) 108 | result))) 109 | 110 | (defn- decode-stream-catching-parse-exceptions 111 | "JSON decode data from given reader making sure proper error handling is in place." 112 | [reader exception-structure] 113 | (let [result (catching-parse-exceptions (json/decode-stream reader keyword) exception-structure)] 114 | (if (seq? result) 115 | (lazy-seq-catching-parse-exceptions result exception-structure) 116 | result))) 117 | 118 | (s/defn ^:always-validate GET 119 | "Make a GET request using the given PuppetDB client, returning the results 120 | as a clojure data structure. If the structure contains any maps then keys 121 | in those maps will be keywordized. 122 | 123 | The `path` argument must be a URL-encoded string. 124 | 125 | You may provide a set of querystring parameters as a map. These will be url-encoded 126 | automatically and added to the path." 127 | ([client path params] 128 | {:pre [(satisfies? PdbClient client) (instance? String path) (map? params)]} 129 | (let [query-info (-> (client-info client) 130 | (assoc :endpoint path) 131 | (assoc :params params)) 132 | connection-error-structure (assoc-kind query-info :puppetdb-connection-error) 133 | response (-> (pdb-get client path params) 134 | (catching-exceptions connection-error-structure))] 135 | 136 | (when-let [exception (:error response)] 137 | (throw+ (assoc connection-error-structure :exception exception))) 138 | 139 | (if-not (= 200 (:status response)) 140 | (throw+ (-> query-info 141 | (assoc-kind :puppetdb-query-error) 142 | (assoc :status (:status response)) 143 | (assoc :msg (slurp (make-response-reader response)))))) 144 | (let [data (-> response 145 | make-response-reader 146 | (decode-stream-catching-parse-exceptions query-info)) 147 | headers (:headers response)] 148 | [data headers]))) 149 | 150 | ([client path] 151 | (GET client path {}))) 152 | -------------------------------------------------------------------------------- /test/clj_puppetdb/vcr_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-puppetdb.vcr-test 2 | (:require [clojure.test :refer :all] 3 | [clj-puppetdb.core :refer [connect query-with-metadata]] 4 | [clj-puppetdb.http-core :refer :all] 5 | [clj-puppetdb.http :refer :all] 6 | [clj-puppetdb.vcr :as vcr] 7 | [puppetlabs.http.client.async :as async] 8 | [me.raynes.fs :as fs])) 9 | 10 | (def mock-http-response-template 11 | (-> {:opts {:persistent false 12 | :as :stream 13 | :decompress-body true 14 | :body nil 15 | :headers {} 16 | :method :get 17 | :url "http://pe:8080/v4/nodes"} 18 | :orig-content-encoding "gzip" 19 | :status 200 20 | :headers {"x-records" "12345" 21 | "content-type" "application/json; charset=iso-8859-1"}} 22 | vcr/rebuild-content-type)) 23 | 24 | (deftest vcr-test 25 | (testing "VCR recording and replay" 26 | (let [vcr-dir "vcr-test" 27 | http-client (async/create-client {})] 28 | (fs/delete-dir vcr-dir) 29 | (testing "when VCR is enabled" 30 | (let [conn (connect http-client "http://localhost:8080" {:vcr-dir vcr-dir})] 31 | (is (= vcr-dir (:vcr-dir (client-info conn)))) 32 | (testing "and no recording exists" 33 | (with-redefs [async/request-with-client 34 | (fn [_ _ _] (future 35 | ; Return mock data 36 | (-> mock-http-response-template 37 | (assoc :body " {\"test\": \"all-nodes\"} ") 38 | vcr/body->stream)))] 39 | ; Real response, should be recorded 40 | (is (= [{:test "all-nodes"} {:total 12345}] (query-with-metadata conn "/v4/nodes" nil)))) 41 | (with-redefs [async/request-with-client 42 | (fn [_ _ _] (future 43 | ; Return mock data 44 | (-> mock-http-response-template 45 | (assoc :body " {\"test\": \"some-nodes\"} ") 46 | vcr/body->stream)))] 47 | ; Real response, should be recorded 48 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 49 | conn 50 | "/v4/nodes" 51 | [:= :certname "test"] 52 | (array-map :limit 1 :order-by [(array-map :field :receive-time :order "desc")])))) 53 | ; Real response, but should not be recorded again 54 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 55 | conn 56 | "/v4/nodes" 57 | [:= :certname "test"] 58 | (array-map :order-by [(array-map :field :receive-time :order "desc")] :limit 1)))) 59 | ; Real response, but should not be recorded again 60 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 61 | conn 62 | "/v4/nodes" 63 | [:= :certname "test"] 64 | (array-map :order-by [(array-map :order "desc" :field :receive-time)] :limit 1))))) 65 | ; There should be 2 recordings 66 | (is (= 2 (count (fs/list-dir vcr-dir))))) 67 | (testing "and a recording already exists" 68 | (is (= [{:test "all-nodes"} {:total 12345}] (query-with-metadata conn "/v4/nodes" nil))) 69 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 70 | conn 71 | "/v4/nodes" 72 | [:= :certname "test"] 73 | (array-map :limit 1 :order-by [(array-map :field :receive-time :order "desc")])))) 74 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 75 | conn 76 | "/v4/nodes" 77 | [:= :certname "test"] 78 | (array-map :order-by [(array-map :order "desc" :field :receive-time)] :limit 1))))) 79 | (testing "and a recording already exists and the real endpoint has changed" 80 | (with-redefs [async/request-with-client 81 | (fn [_ _ _] 82 | ; This should not be called as all the responses should be read from the VCR files. 83 | (throw (RuntimeException. "this should actually never be called")))] 84 | ; VCR enabled so we expect to see the original bodies 85 | (is (= [{:test "all-nodes"} {:total 12345}] (query-with-metadata conn "/v4/nodes" nil))) 86 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 87 | conn 88 | "/v4/nodes" 89 | [:= :certname "test"] 90 | (array-map :limit 1 :order-by [(array-map :field :receive-time :order "desc")])))) 91 | (is (= [{:test "some-nodes"} {:total 12345}] (query-with-metadata 92 | conn 93 | "/v4/nodes" 94 | [:= :certname "test"] 95 | (array-map :order-by [(array-map :order "desc" :field :receive-time)] :limit 1))))))) 96 | (testing "when VCR is not enabled but a recording exists" 97 | (let [conn (connect http-client "http://localhost:8080")] 98 | (is (not (contains? (client-info conn) :vcr-dir))) 99 | (with-redefs [async/request-with-client 100 | (fn [_ _ _] (future 101 | ; Return mock data 102 | (-> mock-http-response-template 103 | (assoc :body " {\"test\": \"all-nodes-changed\"} ") 104 | vcr/body->stream)))] 105 | ; Real response, should not be recorded 106 | (is (= [{:test "all-nodes-changed"} {:total 12345}] (query-with-metadata conn "/v4/nodes" nil)))) 107 | (with-redefs [async/request-with-client 108 | (fn [_ _ _] (future 109 | ; Return mock data 110 | (-> mock-http-response-template 111 | (assoc :body " {\"test\": \"some-nodes-changed\"} ") 112 | vcr/body->stream)))] 113 | ; Real response, should not be recorded 114 | (is (= [{:test "some-nodes-changed"} {:total 12345}] (query-with-metadata 115 | conn 116 | "/v4/nodes" 117 | [:= :certname "test"] 118 | (array-map :limit 1 :order-by [(array-map :field :receive-time :order "desc")])))) 119 | (is (= [{:test "some-nodes-changed"} {:total 12345}] (query-with-metadata 120 | conn 121 | "/v4/nodes" 122 | [:= :certname "test"] 123 | (array-map :order-by [(array-map :order "desc" :field :receive-time)] :limit 1))))) 124 | ; There should still be just 2 recordings 125 | (is (= 2 (count (fs/list-dir vcr-dir))))) 126 | (fs/delete-dir vcr-dir)))))) 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------