From 49a7d685e4c0e15477d943340f8b624502aa3721 Mon Sep 17 00:00:00 2001 From: greg Date: Tue, 7 Nov 2023 19:51:02 +0000 Subject: [PATCH] Switch from `jsonpath_lib` to `jsonpath-rust` `jsonpath_lib` is unmaintained, which is not ideal. It is also forcing everyone to enable a `preserve_order` feature in the `serde_json` dependency. `preserve_order` is not following the proper feature model, where features are only adding functionality. It is actually changing how `serde_json` works, switching from `BTreeMap` to `IndexMap`. Signed-off-by: Illia Bobyr --- examples/Cargo.toml | 2 +- examples/dynamic_jsonpath.rs | 24 +++++++++++----- kube-client/Cargo.toml | 4 +-- kube-client/src/client/auth/mod.rs | 45 +++++++++++++++++++----------- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 8ae0e5fe1..8701d864a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -30,7 +30,7 @@ assert-json-diff = "2.0.1" garde = { version = "0.16.1", default-features = false, features = ["derive"] } anyhow = "1.0.44" futures = "0.3.17" -jsonpath_lib = "0.3.0" +jsonpath-rust = "0.3.4" kube = { path = "../kube", version = "^0.87.1", default-features = false, features = ["admission"] } kube-derive = { path = "../kube-derive", version = "^0.87.1", default-features = false } # only needed to opt out of schema k8s-openapi = { version = "0.20.0", default-features = false } diff --git a/examples/dynamic_jsonpath.rs b/examples/dynamic_jsonpath.rs index 347b1cbbb..ece418d1f 100644 --- a/examples/dynamic_jsonpath.rs +++ b/examples/dynamic_jsonpath.rs @@ -1,8 +1,11 @@ +use anyhow::{Context, Error}; +use jsonpath_rust::JsonPathInst; use k8s_openapi::api::core::v1::Pod; use kube::{ api::{Api, ListParams}, Client, }; +use std::str::FromStr; use tracing::*; #[tokio::main] @@ -13,11 +16,17 @@ async fn main() -> anyhow::Result<()> { // Equivalent to `kubectl get pods --all-namespace \ // -o jsonpath='{.items[*].spec.containers[*].image}'` let field_selector = std::env::var("FIELD_SELECTOR").unwrap_or_default(); - let jsonpath = format!( - "{}{}", - "$", - std::env::var("JSONPATH").unwrap_or_else(|_| ".items[*].spec.containers[*].image".into()) - ); + let jsonpath = { + let path = std::env::var("JSONPATH").unwrap_or_else(|_| ".items[*].spec.containers[*].image".into()); + JsonPathInst::from_str(&format!("{}{}", "$", path.clone())) + .map_err(Error::msg) + .with_context(|| { + format!( + "Failed to parse 'JSONPATH' value as a JsonPath expression.\n + Got: {path}" + ) + })? + }; let pods: Api = Api::::all(client); let list_params = ListParams::default().fields(&field_selector); @@ -25,7 +34,8 @@ async fn main() -> anyhow::Result<()> { // Use the given JSONPATH to filter the ObjectList let list_json = serde_json::to_value(&list)?; - let res = jsonpath_lib::select(&list_json, &jsonpath).unwrap(); - info!("\t\t {:?}", res); + for res in jsonpath.find_slice(&list_json) { + info!("\t\t {}", *res); + } Ok(()) } diff --git a/kube-client/Cargo.toml b/kube-client/Cargo.toml index 101f53bcb..39ff58eae 100644 --- a/kube-client/Cargo.toml +++ b/kube-client/Cargo.toml @@ -23,7 +23,7 @@ ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws", "tokio/macros"] oauth = ["client", "tame-oauth"] oidc = ["client", "form_urlencoded"] gzip = ["client", "tower-http/decompression-gzip"] -client = ["config", "__non_core", "hyper", "http-body", "tower", "tower-http", "hyper-timeout", "pin-project", "chrono", "jsonpath_lib", "bytes", "futures", "tokio", "tokio-util", "either"] +client = ["config", "__non_core", "hyper", "http-body", "tower", "tower-http", "hyper-timeout", "pin-project", "chrono", "jsonpath-rust", "bytes", "futures", "tokio", "tokio-util", "either"] jsonpatch = ["kube-core/jsonpatch"] admission = ["kube-core/admission"] config = ["__non_core", "pem", "home"] @@ -56,7 +56,7 @@ rustls-pemfile = { version = "1.0.0", optional = true } bytes = { version = "1.1.0", optional = true } tokio = { version = "1.14.0", features = ["time", "signal", "sync"], optional = true } kube-core = { path = "../kube-core", version = "=0.87.1" } -jsonpath_lib = { version = "0.3.0", optional = true } +jsonpath-rust = { version = "0.3.4", optional = true } tokio-util = { version = "0.7.0", optional = true, features = ["io", "codec"] } hyper = { version = "0.14.13", optional = true, features = ["client", "http1", "stream", "tcp"] } hyper-rustls = { version = "0.24.0", optional = true } diff --git a/kube-client/src/client/auth/mod.rs b/kube-client/src/client/auth/mod.rs index 3c5a42933..384900de5 100644 --- a/kube-client/src/client/auth/mod.rs +++ b/kube-client/src/client/auth/mod.rs @@ -10,9 +10,10 @@ use http::{ header::{InvalidHeaderValue, AUTHORIZATION}, HeaderValue, Request, }; -use jsonpath_lib::select as jsonpath_select; +use jsonpath_rust::JsonPathInst; use secrecy::{ExposeSecret, SecretString}; use serde::{Deserialize, Serialize}; +use std::str::FromStr; use thiserror::Error; use tokio::sync::{Mutex, RwLock}; use tower::{filter::AsyncPredicate, BoxError}; @@ -440,9 +441,9 @@ fn token_from_gcp_provider(provider: &AuthProviderConfig) -> Result>() .map_err(Error::MalformedTokenExpirationDate)?; @@ -474,22 +475,32 @@ fn token_from_gcp_provider(provider: &AuthProviderConfig) -> Result Result { - let pure_path = path.trim_matches(|c| c == '"' || c == '{' || c == '}'); - match jsonpath_select(json, &format!("${pure_path}")) { - Ok(v) if !v.is_empty() => { - if let serde_json::Value::String(res) = v[0] { - Ok(res.clone()) - } else { - Err(Error::AuthExec(format!( - "Target value at {pure_path:} is not a string" - ))) - } - } +fn extract_value(json: &serde_json::Value, context: &str, path: &str) -> Result { + let parsed_path = { + let trimmed = path.trim_matches(|c| c == '"' || c == '{' || c == '}'); + JsonPathInst::from_str(trimmed).map_err(|err| { + Error::AuthExec(format!( + "Failed to parse {context:?} as a JsonPath: {path}\n + Error: {err}" + )) + })? + }; - Err(e) => Err(Error::AuthExec(format!("Could not extract JSON value: {e:}"))), + let res = parsed_path.find_slice(json); - _ => Err(Error::AuthExec(format!("Target value {pure_path:} not found"))), + let Some(res) = res.into_iter().next() else { + return Err(Error::AuthExec(format!( + "Target {context:?} value {path:?} not found" + ))); + }; + + if let Some(val) = res.as_str() { + Ok(val.to_owned()) + } else { + Err(Error::AuthExec(format!( + "Target {:?} value {:?} is not a string: {:?}", + context, path, *res + ))) } }