From 69ccd48c3b80940578f4a43b10b934a330aa305c Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 10:30:53 +0500 Subject: [PATCH 01/10] fix(controller/generator)!: take iterator for chars to reduce memory overhead --- kuyaseg-controller/src/generator.rs | 28 ++++++++++++++++------------ kuyaseg-controller/src/lib.rs | 3 +-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/kuyaseg-controller/src/generator.rs b/kuyaseg-controller/src/generator.rs index ad6a384..f56fb60 100644 --- a/kuyaseg-controller/src/generator.rs +++ b/kuyaseg-controller/src/generator.rs @@ -30,10 +30,14 @@ pub fn bytes(rng: &mut G, size: usize) -> Vec { buf } -pub fn str_bytes(rng: &mut G, length: usize, dictionary: &[char]) -> Result, GeneratorErr> { +pub fn str_bytes( + rng: &mut G, + length: usize, + dictionary: impl Iterator, +) -> Result, GeneratorErr> { let mut buf = String::with_capacity(length); - let dictionary = Zeroizing::new(dictionary.iter().unique().cloned().collect::>()); + let dictionary = Zeroizing::new(dictionary.unique().collect::>()); let distribution = rand::distr::slice::Choose::new(&dictionary).map_err(|_| GeneratorErr::EmptyDistribution)?; let dict_size = dictionary.len(); @@ -194,11 +198,11 @@ mod test { let mut rng = AwsRng::new(); let dict = dict_alnum(32); - let gen0 = str_bytes(&mut rng, 0, &dict).unwrap(); - let gen1 = str_bytes(&mut rng, 1, &dict).unwrap(); - let gen27 = str_bytes(&mut rng, 27, &dict).unwrap(); - let gen256 = str_bytes(&mut rng, 256, &dict).unwrap(); - assert_ne!(str_bytes(&mut rng, 256, &dict).unwrap(), gen256); + let gen0 = str_bytes(&mut rng, 0, dict.iter().cloned()).unwrap(); + let gen1 = str_bytes(&mut rng, 1, dict.iter().cloned()).unwrap(); + let gen27 = str_bytes(&mut rng, 27, dict.iter().cloned()).unwrap(); + let gen256 = str_bytes(&mut rng, 256, dict.iter().cloned()).unwrap(); + assert_ne!(str_bytes(&mut rng, 256, dict.iter().cloned()).unwrap(), gen256); // length assert_eq!(gen0.len(), 0); @@ -222,7 +226,7 @@ mod test { let mut rng = AwsRng::new(); let dict: Vec = dict_alnum(3); - let gen16 = str_bytes(&mut rng, 16, &dict).unwrap(); + let gen16 = str_bytes(&mut rng, 16, dict.iter().cloned()).unwrap(); assert_eq!(gen16.len(), 16); assert_dict_chars(&gen16, &dict); @@ -258,12 +262,12 @@ mod test { let dict = dict_alnum(32); assert_eq!( - str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap(), - str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap() + str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, dict.iter().cloned()).unwrap(), + str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, dict.iter().cloned()).unwrap() ); assert_ne!( - str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap(), - str_bytes(&mut Hc128Rng::seed_from_u64(43), 256, &dict).unwrap() + str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, dict.iter().cloned()).unwrap(), + str_bytes(&mut Hc128Rng::seed_from_u64(43), 256, dict.iter().cloned()).unwrap() ); } diff --git a/kuyaseg-controller/src/lib.rs b/kuyaseg-controller/src/lib.rs index fed3523..c185f23 100644 --- a/kuyaseg-controller/src/lib.rs +++ b/kuyaseg-controller/src/lib.rs @@ -1,5 +1,4 @@ -#![feature(sync_nonpoison, nonpoison_rwlock)] - pub mod generator; pub mod reporting; +pub mod resource; pub mod utils; From 1e7d71ffc390dfb1f09e862f34132feddc48b011 Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 13:43:15 +0500 Subject: [PATCH 02/10] feat(controller): introduce basic, UNREADY generation --- Cargo.lock | 1 + Cargo.toml | 1 + kuyaseg-controller/Cargo.toml | 1 + kuyaseg-controller/src/bin/generate_crds.rs | 15 +- .../src/resource/generated/mod.rs | 2 + .../src/resource/generated/secret/mod.rs | 577 ++++++++++++++++++ .../src/resource/generated/secret/v1alpha1.rs | 182 ++++++ .../src/resource/generated/ssh/mod.rs | 1 + kuyaseg-controller/src/resource/mod.rs | 1 + 9 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 kuyaseg-controller/src/resource/generated/mod.rs create mode 100644 kuyaseg-controller/src/resource/generated/secret/mod.rs create mode 100644 kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs create mode 100644 kuyaseg-controller/src/resource/generated/ssh/mod.rs create mode 100644 kuyaseg-controller/src/resource/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1cdd132..2d22c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1599,6 +1599,7 @@ dependencies = [ "ssh-key", "thiserror", "tokio", + "tokio-util", "tracing", "tracing-opentelemetry", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 2898c1a..9015c63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ mimalloc.version = "^0.1" mimalloc.features = ["secure"] tokio.version = "^1" tokio.features = ["macros", "rt-multi-thread"] +tokio-util.version = '^0.7' axum.version = "^0.8" ## Tracing tracing.version = "^0.1" diff --git a/kuyaseg-controller/Cargo.toml b/kuyaseg-controller/Cargo.toml index 0e6e153..5435d05 100644 --- a/kuyaseg-controller/Cargo.toml +++ b/kuyaseg-controller/Cargo.toml @@ -18,6 +18,7 @@ path = "src/bin/generate_crds.rs" ## Rt mimalloc.workspace = true tokio.workspace = true +tokio-util.workspace = true axum.workspace = true ## Tracing tracing.workspace = true diff --git a/kuyaseg-controller/src/bin/generate_crds.rs b/kuyaseg-controller/src/bin/generate_crds.rs index e71fdf5..4390cf7 100644 --- a/kuyaseg-controller/src/bin/generate_crds.rs +++ b/kuyaseg-controller/src/bin/generate_crds.rs @@ -1 +1,14 @@ -fn main() {} \ No newline at end of file +use kube::CustomResourceExt; + +fn main() { + print!( + "{}", + serde_yaml::to_string(&kuyaseg_controller::resource::generated::secret::v1alpha1::RandomSecret::crd()) + .expect("RandomSecret v1alpha1 CRD") + ); + print!( + "{}", + serde_yaml::to_string(&kuyaseg_controller::resource::generated::secret::v1alpha1::DeterministicSecret::crd()) + .expect("DeterministicSecret v1alpha1 CRD") + ) +} diff --git a/kuyaseg-controller/src/resource/generated/mod.rs b/kuyaseg-controller/src/resource/generated/mod.rs new file mode 100644 index 0000000..899b535 --- /dev/null +++ b/kuyaseg-controller/src/resource/generated/mod.rs @@ -0,0 +1,2 @@ +pub mod secret; +pub mod ssh; diff --git a/kuyaseg-controller/src/resource/generated/secret/mod.rs b/kuyaseg-controller/src/resource/generated/secret/mod.rs new file mode 100644 index 0000000..cfb12e3 --- /dev/null +++ b/kuyaseg-controller/src/resource/generated/secret/mod.rs @@ -0,0 +1,577 @@ +use std::collections::{ + BTreeMap, + HashMap, +}; + +use anyhow::Context; +use kube::{ + Api, + Resource, +}; +use schemars::JsonSchema; +use serde::de::DeserializeOwned; +use serde::{ + Deserialize, + Serialize, +}; +use serde_json::json; +use zeroize::Zeroize; + +pub mod v1alpha1; + +use crate::utils::zeroizers::{ + self, + zeroize_btree, + zeroize_hashmap, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GeneratedSecretSpec { + #[serde(default)] + pub keys: Vec, + #[serde(default)] + pub template: GeneratedSecretTemplate, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GeneratedSecretStatus { + /// Stored all generated keys for matching v1/Secret resource, hashed by key name. + /// The matcher value is (version, shape hash) + /// + /// Shape hash is obtained with Blake3 hash. + pub generated: BTreeMap, +} + +impl GeneratedSecretStatus { + /// Determine whether the key from spec was already generated, based on: + /// - Key name + /// - Key version + /// - Key shape hash + fn was_generated(&self, spec: &GeneratedSecretKey) -> bool { + let Some(stored) = self.generated.get(&spec.key) else { + return false; + }; + + (stored.0 == spec.version) + && aws_lc_rs::constant_time::verify_slices_are_equal( + stored.1.as_bytes(), + spec.shape.stable_hash().as_bytes(), + ) + .is_ok() + } +} + +impl GeneratedSecretSpec { + /// Reconcile generated secret from specification, following the specific type `T` generation strategy. + /// This method propagates upstream. + /// + /// This method family assumes that upstream resource has `GeneratedSecretStatus` status. + /// + /// I wrote this because - hell no - I won't write this code twice for both Random and Deterministic secrets! + async fn reconcile_spec(&self, name: &str, namespace: &str, client: kube::Client) -> anyhow::Result<()> + where + T: HasSecretGenerator + + Resource + + DeserializeOwned, + { + // discover target secret & branch for update/create + let target_api = Api::::namespaced(client.clone(), namespace); + let maybe_secret = scopeguard::guard( + { + target_api + .get_opt(name) + .await + .inspect_err(|e| { + tracing::error!( + debug=?e, + critical = true, + reason = "SecretDiscoveryFailed", + action = "Reconcile", + "Failed to discover target Secret" + ) + }) + .context("Discover target secret")? + }, + |mut v| { + v.as_mut().map(zeroizers::zeroize_k8s_secret); + }, + ); + + match maybe_secret.as_ref() { + Some(yep) => self + .update_from_spec::(name, namespace, client, yep) + .await + .context("Update existing Secret")?, + None => self + .create_new_from_spec::(name, namespace, client) + .await + .context("Update existing Secret")?, + } + + Ok(()) + } + + async fn create_new_from_spec(&self, name: &str, namespace: &str, client: kube::Client) -> anyhow::Result<()> + where + T: HasSecretGenerator + + Resource + + DeserializeOwned, + { + let self_api = Api::::namespaced(client.clone(), namespace); + let target_api = Api::::namespaced(client.clone(), namespace); + + let generated = self + .keys + .iter() + .map(|key| (key.key.clone(), (key.version, key.shape.stable_hash()))) + .collect::>(); + + // first we try with dry-run, no need to guard as no data here + let partial_target = self.template.to_safe_partial_secret(Some(false), Some("Opaque".into())); + let patch = kube::api::Patch::Apply(json!({ + "apiVersion": T::api_version(&()), + "kind": T::kind(&()), + "status": GeneratedSecretStatus { + generated + } + })); + + // dry-run + { + target_api + .create( + &kube::api::PostParams { + dry_run: true, + field_manager: Some(env!("CARGO_PKG_NAME").into()), + }, + &partial_target, + ) + .await + .inspect_err(|e| { + tracing::error!( + debug=?e, + reason = "SecretCreationFailed", + action = "CreateNew", + "Failed to create Secret resource" + ) + }) + .context("Dry-run create Secret")?; + + self_api + .patch_status( + name, + &kube::api::PatchParams { + dry_run: true, + force: true, + field_manager: Some(env!("CARGO_PKG_NAME").into()), + field_validation: Some(kube::api::ValidationDirective::Strict), + }, + &patch, + ) + .await.inspect_err(|e| tracing::error!(debug=?e, reason="SelfUpdateFailed", action = "CreateNew", "Failed to update self status")) + .context("Dry-run update status")?; + } + + // desperate run + { + let target = scopeguard::guard( + k8s_openapi::api::core::v1::Secret { + data: Some(T::generate_data(self).context("Create new Secret")?), + ..partial_target + }, + |mut v| zeroizers::zeroize_k8s_secret(&mut v), + ); + + target_api + .create( + &kube::api::PostParams { + dry_run: false, + field_manager: Some(env!("CARGO_PKG_NAME").into()), + }, + &target, + ) + .await + .inspect_err(|e| { + tracing::error!( + debug=?e, + reason = "SecretCreationFailed", + action = "CreateNew", + "Failed to create Secret resource" + ) + }) + .context("Create Secret")?; + + // TODO: delete created secret if this fails or not..? + self_api + .patch_status( + name, + &kube::api::PatchParams { + dry_run: false, + force: true, + field_manager: Some(env!("CARGO_PKG_NAME").into()), + field_validation: Some(kube::api::ValidationDirective::Strict), + }, + &patch, + ) + .await.inspect_err(|e| tracing::error!(debug=?e, reason="SelfUpdateFailed", action = "CreateNew", "Failed to update self status")) + .context("Update status")?; + } + + Ok(()) + } + + async fn update_from_spec( + &self, + name: &str, + namespace: &str, + client: kube::Client, + old_ref: &k8s_openapi::api::core::v1::Secret, + ) -> anyhow::Result<()> + where + T: HasSecretGenerator + + Resource + + DeserializeOwned, + { + todo!("WIP") + } +} + +pub(super) trait HasSecretGenerator { + fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result>; +} + +#[cfg_attr(any(debug_assertions, test), derive(Debug, Eq, PartialEq))] +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GeneratedSecretTemplate { + /// Add custom metadata to generated secret + #[serde(default)] + pub metadata: GeneratedSecretMetadata, + /// Base64 (standard+pad) encoded data + #[serde(default)] + pub data: HashMap, + /// Verbatim string data, which will be incoded as bytes into secret + #[serde(rename = "stringData")] + #[serde(default)] + pub string_data: BTreeMap, +} + +impl GeneratedSecretTemplate { + /// Does not retain data. + pub fn to_safe_partial_secret( + &self, + immutable: Option, + type_: Option, + ) -> k8s_openapi::api::core::v1::Secret { + k8s_openapi::api::core::v1::Secret { + type_, + immutable, + data: None, + metadata: self.metadata.clone().into_partial_metadata(), + string_data: None, + } + } +} + +impl Zeroize for GeneratedSecretTemplate { + fn zeroize(&mut self) { + zeroize_btree(&mut self.string_data); + zeroize_hashmap(&mut self.data); + } +} + +impl Drop for GeneratedSecretTemplate { + fn drop(&mut self) { + self.zeroize(); + } +} + +#[cfg_attr(any(debug_assertions, test), derive(Debug, Eq, PartialEq))] +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GeneratedSecretMetadata { + /// Add annotations to generated secret + #[serde(default)] + pub annotations: BTreeMap, + /// Add labels to generated secret + #[serde(default)] + pub labels: BTreeMap, +} + +impl GeneratedSecretMetadata { + pub fn into_partial_metadata(self) -> kube::api::ObjectMeta { + kube::api::ObjectMeta { + annotations: Some(self.annotations), + labels: Some(self.labels), + ..Default::default() + } + } +} + +#[cfg_attr(any(debug_assertions, test), derive(Debug, PartialEq, Eq))] +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct GeneratedSecretKey { + /// The name of the key to write generated value into + pub key: String, + /// The version of generated value. Changing this will force new value to be generated + pub version: usize, + /// The type/kind specification for the generated value. May be `password` or `bytes`. Contains subfields + pub shape: GeneratedSecretShape, +} + +#[cfg_attr(any(debug_assertions, test), derive(Debug, PartialEq, Eq))] +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub enum GeneratedSecretShape { + Password { + /// The length of password in characters. Defaults to 32 + #[serde(default = "GeneratedSecretShape::default_pwd_length")] + length: usize, + /// The string of characters generator allowed to use. Defaults to alphanumeric lowercase+uppercase + #[serde(default = "GeneratedSecretShape::default_pwd_dict")] + dictionary: String, + }, + Bytes { + /// The size of secret in bytes. Defaults to 64 + #[serde(default = "GeneratedSecretShape::default_bytes_size")] + size: usize, + }, +} + +impl GeneratedSecretShape { + /// Base64 encoded stable (blake3) hash + pub fn stable_hash(&self) -> String { + use base64::prelude::*; + + let hash = { + let mut hasher = blake3::Hasher::new(); + match self { + Self::Bytes { size } => { + hasher.update(&size.to_le_bytes()); + } + Self::Password { length, dictionary } => { + hasher.update(&length.to_le_bytes()); + hasher.update(dictionary.as_bytes()); + } + } + hasher.finalize() + }; + + BASE64_STANDARD.encode(hash.as_bytes()) + } + + pub fn default_pwd_length() -> usize { + 32 + } + + pub fn default_pwd_dict() -> String { + String::from("abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ") + } + + pub fn default_bytes_size() -> usize { + 64 + } +} + +impl Zeroize for GeneratedSecretShape { + fn zeroize(&mut self) { + match self { + Self::Bytes { size } => { + size.zeroize(); + } + Self::Password { length, dictionary } => { + length.zeroize(); + dictionary.zeroize(); + } + } + } +} + +impl Drop for GeneratedSecretShape { + fn drop(&mut self) { + self.zeroize(); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn deser_template_metadata() { + // Full sample + let sample = " + annotations: + my.custom-annotation: value + labels: + some-custom.label: value + "; + let expect = GeneratedSecretMetadata { + annotations: BTreeMap::from_iter([("my.custom-annotation".into(), "value".into())]), + labels: BTreeMap::from_iter([("some-custom.label".into(), "value".into())]), + }; + let parsed: GeneratedSecretMetadata = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Full sample didn't match"); + + // Empty annotations & labels + let sample = " + annotations: {} + labels: {} + "; + let expect = GeneratedSecretMetadata { + annotations: BTreeMap::new(), + labels: BTreeMap::new(), + }; + let parsed: GeneratedSecretMetadata = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty annotations&labels didn't match"); + + // Empty + let sample = ""; + let expect = GeneratedSecretMetadata::default(); + let parsed: GeneratedSecretMetadata = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty didn't match"); + } + + #[test] + fn deser_template() { + // full sample + let sample = " + data: + key0: abcde + stringData: + key1: fghij + metadata: + annotations: + my.custom-annotation: value + labels: + some-custom.label: value + "; + let expect = GeneratedSecretTemplate { + data: HashMap::from_iter([("key0".into(), "abcde".into())]), + string_data: BTreeMap::from_iter([("key1".into(), "fghij".into())]), + metadata: GeneratedSecretMetadata { + annotations: BTreeMap::from_iter([("my.custom-annotation".into(), "value".into())]), + labels: BTreeMap::from_iter([("some-custom.label".into(), "value".into())]), + }, + }; + let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Full sample didn't match"); + + // Empty metadata + let sample = " + data: + key0: abcde + stringData: + key1: fghij + metadata: {} + "; + let expect = GeneratedSecretTemplate { + data: HashMap::from_iter([("key0".into(), "abcde".into())]), + string_data: BTreeMap::from_iter([("key1".into(), "fghij".into())]), + metadata: GeneratedSecretMetadata::default(), + }; + let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty metadata didn't match"); + + // No metadata + let sample = " + data: + key0: abcde + stringData: + key1: fghij + "; + let expect = GeneratedSecretTemplate { + data: HashMap::from_iter([("key0".into(), "abcde".into())]), + string_data: BTreeMap::from_iter([("key1".into(), "fghij".into())]), + metadata: GeneratedSecretMetadata::default(), + }; + let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "No metadata didn't match"); + + // Empty data & stringdata + let sample = " + data: {} + stringData: {} + "; + let expect = GeneratedSecretTemplate { + data: HashMap::new(), + string_data: BTreeMap::new(), + metadata: GeneratedSecretMetadata::default(), + }; + let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty data&stringData didn't match"); + + // Empty + let sample = ""; + let expect = GeneratedSecretTemplate::default(); + let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty didn't match"); + } + + #[test] + fn deser_key_spec() { + // Full password + let sample = " + key: aabbcc + version: 12 + shape: !Password + length: 123 + dictionary: abcdefg_1 + "; + let expect = GeneratedSecretKey { + key: "aabbcc".into(), + version: 12, + shape: GeneratedSecretShape::Password { + length: 123, + dictionary: "abcdefg_1".into(), + }, + }; + let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Full password mismatch"); + + // Empty password + let sample = " + key: aabbcc + version: 12 + shape: !Password + "; + let expect = GeneratedSecretKey { + key: "aabbcc".into(), + version: 12, + shape: GeneratedSecretShape::Password { + length: GeneratedSecretShape::default_pwd_length(), + dictionary: GeneratedSecretShape::default_pwd_dict(), + }, + }; + let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Empty password mismatch"); + + // Full bytes + let sample = " + key: aabbcc + version: 12 + shape: !Bytes + size: 1234 + "; + let expect = GeneratedSecretKey { + key: "aabbcc".into(), + version: 12, + shape: GeneratedSecretShape::Bytes { size: 1234 }, + }; + let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Full bytes mismatch"); + + // Empty bytes + let sample = " + key: aabbcc + version: 12 + shape: !Bytes + "; + let expect = GeneratedSecretKey { + key: "aabbcc".into(), + version: 12, + shape: GeneratedSecretShape::Bytes { + size: GeneratedSecretShape::default_bytes_size(), + }, + }; + let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Empty bytes mismatch"); + } +} diff --git a/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs b/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs new file mode 100644 index 0000000..5df999b --- /dev/null +++ b/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs @@ -0,0 +1,182 @@ +use std::collections::BTreeMap; + +use anyhow::Context; +use k8s_openapi::ByteString; +use kube::runtime::controller::Action; +use kube::{ + Client, + CustomResource, + Resource, + ResourceExt, +}; +use rand::prelude::SeedableRng; +use rand_hc::Hc128Rng; +use schemars::JsonSchema; +use serde::{ + Deserialize, + Serialize, +}; + +use crate::generator; +use crate::resource::generated::secret::{ + GeneratedSecretShape, + GeneratedSecretSpec, + GeneratedSecretStatus, + HasSecretGenerator, +}; + +/// Spec to generate random v1/Secret resource. +/// The resource is generated with the same name in the same namespace as this CR is deployed. +/// +/// The generated Secret data will change: +/// - On next reconcilation when: +/// - Generated v1/Secret was deleted (all regen) +/// - Key spec was changed (per-key regen) +/// +/// Altering the `template` *will not* change generated values +#[derive(Debug, Clone, Serialize, Deserialize, CustomResource, JsonSchema)] +#[kube( + namespaced, + kind = "RandomSecret", + group = "generator.kuyaseg.spushk.ru", + version = "v1alpha1", + status = "GeneratedSecretStatus" +)] +#[kube( + singular = "randomsecret", + plural = "randomsecrets", + shortname = "randsec", + category = "all", + category = "generators", + category = "secret-generators" +)] +#[serde(transparent)] +pub struct RandomSecretSpec(GeneratedSecretSpec); + +impl RandomSecret { + pub async fn reconcile(&self, client: Client) -> anyhow::Result { + let kind = Self::kind(&()); + let api = Self::api_version(&()); + let name = &self.name_any(); + let _span_nons = + tracing::info_span!("RandomSecret.reconcile", kind = kind.as_ref(), api = api.as_ref(), name).entered(); + + let Some(namespace) = self.namespace() else { + tracing::error!( + reason = "RandomSecretNeedNamespace", + action = "Reconcile", + "RandomSecret must have namespace" + ); + return Err(anyhow::Error::msg("RandomSecret needed namespace")); + }; + let _span = tracing::info_span!("RandomSecret.reconcile.ns", ns = namespace).entered(); + + self.spec.0.reconcile_spec::(name, &namespace, client).await.inspect_err(|e| tracing::error!(debug=?e, reason="RandomSecretReconcilationFailed", action = "Reconcile", "Failed to reconcile RandomSecret {namespace}{name}")).context("Reconcile")?; + + Ok(Action::await_change()) + } +} + +impl HasSecretGenerator for RandomSecret { + fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result> { + let mut rng = generator::AwsRng::new(); + let mut data = BTreeMap::new(); + + for key in spec.keys.iter() { + match &key.shape { + GeneratedSecretShape::Bytes { size } => { + data.insert(key.key.clone(), ByteString(generator::bytes(&mut rng, *size))); + } + GeneratedSecretShape::Password { length, dictionary } => { + data.insert(key.key.clone(), ByteString(generator::str_bytes(&mut rng, *length, dictionary.chars()).inspect_err(|e| tracing::error!(debug=?e, reason="PasswordGenFailed", action="Generate", "Failed to generate new random password")).context("Generate password")?)); + } + } + } + + Ok(data) + } +} + +/// Spec to generate DETERMINISTIC v1/Secret resource. +/// The resource is generated with the same name in the same namespace as this CR is deployed. +/// +/// The generated Secret data will change on next reconcilation when key spec was changed (per-key regen) +/// +/// Altering the `template` *will not* change generated values. +/// Removing generated v1/Secret *will not* change generated values. +#[derive(Debug, Clone, Serialize, Deserialize, CustomResource, JsonSchema)] +#[kube( + namespaced, + kind = "DeterministicSecret", + group = "generator.kuyaseg.spushk.ru", + version = "v1alpha1", + status = "GeneratedSecretStatus" +)] +#[kube( + singular = "deterministicsecret", + plural = "deterministicsecrets", + shortname = "detsec", + category = "all", + category = "generators", + category = "secret-generators" +)] +#[serde(transparent)] +pub struct DeterministicSecretSpec(GeneratedSecretSpec); + +impl DeterministicSecret { + pub async fn reconcile(&self, client: Client) -> anyhow::Result { + let kind = Self::kind(&()); + let api = Self::api_version(&()); + let name = &self.name_any(); + let _span_nons = tracing::info_span!( + "DeterministicSecret.reconcile", + kind = kind.as_ref(), + api = api.as_ref(), + name + ) + .entered(); + + let Some(namespace) = self.namespace() else { + tracing::error!( + reason = "DeterministicSecretNeedNamespace", + action = "Reconcile", + "DeterministicSecret must have namespace" + ); + return Err(anyhow::Error::msg("DeterministicSecret needed namespace")); + }; + let _span = tracing::info_span!("DeterministicSecret.reconcile.ns", ns = namespace).entered(); + + self.spec.0.reconcile_spec::(name, &namespace, client).await.inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretReconcilationFailed", action = "Reconcile", "Failed to reconcile DeterministicSecret {namespace}{name}")).context("Reconcile")?; + + Ok(Action::await_change()) + } +} + +impl HasSecretGenerator for DeterministicSecret { + fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result> { + let seed = std::env::var("KUYASEG_SEED").inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretNeedSeed", "Refuse to generate deterministic secret without seet. Set with KUYASEG_SEED env")).context("Get external seed")?; + let mut data = BTreeMap::new(); + + for key in spec.keys.iter() { + let mut rng = { + let mut hasher = blake3::Hasher::new(); + hasher.update(seed.as_bytes()); + hasher.update(key.key.as_bytes()); + hasher.update(&key.version.to_le_bytes()); + hasher.update(key.shape.stable_hash().as_bytes()); + Hc128Rng::from_seed(hasher.finalize().into()) + }; + + match &key.shape { + GeneratedSecretShape::Bytes { size } => { + data.insert(key.key.clone(), ByteString(generator::bytes(&mut rng, *size))); + } + GeneratedSecretShape::Password { length, dictionary } => { + data.insert(key.key.clone(), ByteString(generator::str_bytes(&mut rng, *length, dictionary.chars()).inspect_err(|e| tracing::error!(debug=?e, reason="PasswordGenFailed", action="Generate", "Failed to generate new random password")).context("Generate password")?)); + } + } + } + + Ok(data) + } +} diff --git a/kuyaseg-controller/src/resource/generated/ssh/mod.rs b/kuyaseg-controller/src/resource/generated/ssh/mod.rs new file mode 100644 index 0000000..e51933e --- /dev/null +++ b/kuyaseg-controller/src/resource/generated/ssh/mod.rs @@ -0,0 +1 @@ +pub type GeneratedSshSecretTemplate = super::secret::GeneratedSecretTemplate; diff --git a/kuyaseg-controller/src/resource/mod.rs b/kuyaseg-controller/src/resource/mod.rs new file mode 100644 index 0000000..743aa41 --- /dev/null +++ b/kuyaseg-controller/src/resource/mod.rs @@ -0,0 +1 @@ +pub mod generated; From e15bb3bbc6b40a252d5a2f82359f4816e23e5d59 Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 18:36:41 +0500 Subject: [PATCH 03/10] fix: minor fixes, bring LLM for reviewing as a test --- .gitignore | 2 + README.md | 163 ++++++++++++++++++ kuyaseg-controller/src/generator.rs | 9 +- kuyaseg-controller/src/reporting/k8s_event.rs | 2 +- kuyaseg-controller/src/reporting/mod.rs | 14 +- .../src/reporting/prometheus.rs | 1 + .../src/reporting/tracing_evt.rs | 12 +- .../src/resource/generated/secret/mod.rs | 62 +++++-- .../src/resource/generated/secret/v1alpha1.rs | 29 ++-- opencode.jsonc | 13 ++ 10 files changed, 264 insertions(+), 43 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 opencode.jsonc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaef0ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.opencode diff --git a/README.md b/README.md new file mode 100644 index 0000000..d51daed --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# K8s secret generator + +> *AI*: This was also an exploration of opencode features, so there is some amount of AI-generated boilerplate. + +## Features + +### Generate namespaced secrets from specification + +To generate arbitrary secret deploy `RandomSecret` resource: + +```yaml +apiVersion: generator.kuyaseg.spushk.ru/v1alpha1 +kind: RandomSecret +metadata: + name: secret + namespace: default +spec: + # Specify all keys to generate and put into secret + keys: + # Binary key - random byte sequence + - kind: binary + # The key to put generated value to + key: binary-key + #If changes after last reconcilation, will force regeneration with new value + version: 0 + # Set size of value in bytes + size: 64 + # Password key - guaranteed UTF-8 character sequence with repeat period >= 3 + - kind: password + # The key to put generated value to + key: password-key + #If changes after last reconcilation, will force regeneration with new value + version: 0 + # Set length of password in symbols + length: 24 + # Specify which symbols are allowed to use when generating password + dictionary: "abcdefghijklmnopqrstuvwxyz0123456789_" + # Adjust data, stringData and metadata of generated Secret + template: {} +``` + +#### SSH keys + +To generate SSH key deploy `RandomSsh` resource: + +```yaml +apiVersion: generator.kuyaseg.spushk.ru/v1alpha1 +kind: RandomSsh +metadata: + name: ssh-secret + namespace: default +spec: + # Configure keypair configuration + keypair: + # If changes after last reconcilation, will force regeneration with new keypair + version: 0 + # `ed25519` is default. `rsa` is WIP + algorithm: ed25519 + # optional field to set pubkey comment + comment: mysecret + # Configure SSH private key + identity: + # The key to put generated identity to + key: private-key + # Adjust data, stringData and metadata of generated Secret + template: {} + # Configure SSH public key + pubkey: + # The key to put generated identity to + key: public-key + # Adjust data, stringData and metadata of generated Secret + template: {} +``` + +The following template properties have additional validation: +- `metadata.owner_references`: controller owner is not allowed + +When reconciled, this resource will produce two `v1/Secret` resources in the same namespace: +- `-identity`: private SSH key, which is stored in specified key +- `-pubkey`: public SSH key which is stored in specified key + +Both resources inherit namespace and name (as prefix) from `RandomSsh` resource and are immutable. + +### Deterministic secrets + +> ⚠️ This feature exists primarily for testing purposes, where security is less of a concern then repeatability. +> +> While there are use-cases for predictable secrets in production, you should consider [ESO](https://external-secrets.io) +> with secure external storage to share secrets between clusters and keep secrets stable. +> +> If the seed secret gets compromised - every past and future generated secret can be easliy computed. + +Kuyaseg is able to provide deterministic source for secret and SSH keypair generation, offering idempotent sequence of secret values. + +With deterministic secrets value remains the same even if specification was deleted and created anew. + +This could be useful for testing, debugging and learning in local environments. + +Deterministic secrets are created with `DeterministicSecret` and `DeterministicSsh` resources, matching `RandomSecret` and `RandomSsh` resource schema: + +```yaml +# Schema matches the Secret from generator.kuyaseg.spushk.ru/v1alpha1 +apiVersion: generator.kuyaseg.spushk.ru/v1alpha1 +kind: DeterministicSecret +metadata: + name: deterministic-secret + namespace: testing +spec: + keys: + - key: debug-password + kind: password + length: 12 + template: + stringData: + username: debug-user +``` + +Secret values generated by deterministic generator depend on: +- Global `seed` value set for controller +- `metadata.name` and `metadata.namespace` of `DeterministicSecret` resource +- `spec.keys.*.key` and `spec.key.*.version` for each generated key specified in `DeterministicSecret` resource + +Thus, if seed secret remains stable and DeterministicSecret specification does not change - generated keys remain stable and predictable + +## Feature comparison + +We know there are plenty of decent, mature and well-adopted solutions like the one by [mittwald](https://github.com/mittwald/kubernetes-secret-generator) and even [ESO](https://external-secrets.io) which can also generate secrets. + +Here is a comparison. + +||Kuyaseg|kubernetes-secret-generator|ESO|| +|-|-|-|-|-| +|Generate cryptographically secure namespaced secrets|Hc128 CSPRNG, allows to specify generated **keys per resource**|Cryptographically secure, allows to specify generated **keys per resource**|Cryptographically secure, but generated keys are specified **per generator**|| +|Generate SSH keypairs|Ed25519 and RSA OpenSSH keypairs, private and public keys **stored in separate secrets**|Both secret and public keys are **stored in one secret**|Not supported|| +|Deterministic, reproducible secret generation|Available, optional, based on provided secret data (e.g. SealedSecrets). Stable across clusters|Not supported|Not supported|| +|Secret value stability|Created secrets are
  • immutable
  • created if accidentally deleted
  • don't change unles you ask for it
|Stable across reconcilations, but **changes on secret re-creation**|Only with real **external secret storage** (which ESO is designed for)|| +|Target secret template|Respects almost full `Secret` (v1) spec, including
  • annotations
  • labels
  • owner_references
|[Not supported](https://github.com/mittwald/kubernetes-secret-generator/issues/88)|Allows to specity template in `ExternalSecret`|| + +## Security + +⚠️ This software have never received independent security audit. I am not a security expert. + +--- + +For security issues, please, view the [security policy](https://source.spushk.ru/security) and kindly e-mail +us at [reports@spushk.ru](mailto:reports@spushk.ru?subject=%5BSECURITY%5D%3A%20describe%20the%20issue&body=Thank%20you%20for%20submitting%20security%20report%20%E2%9D%A4%EF%B8%8F%0AWe%20kindly%20ask%20you%20not%20to%20publically%20disclose%20any%20information%20related%20to%20your%20findings%20just%20yet.%0APlease%2C%20fill%20out%20the%20following%20sections%20and%20provide%20any%20additional%20information%20you%27re%20willing%20to%20share.%0A%0A-------------------------------------------------------------------------------%20FINDING%0AWhat%20is%20the%20substance%20of%20an%20issue%3F%0A%0A-------------------------------------------------------------------------------%20AFFECTED%20SOFTWARE%0AWhich%20product%20and%20which%20versions%20is%20affected%3F%0A%0A-------------------------------------------------------------------------------%20REPRODUCTION%0APlease%2C%20share%20PoC%20if%20you%20have%20one%0A%0A-------------------------------------------------------------------------------%20DISCLOSURE%0AWhat%20are%20you%20expectations%20on%20disclosure%3F%0AWe%20kindly%20ask%20you%20not%20to%20share%20any%20information%20publically%0Aat%20least%20for%2090%20days%20since%20this%20report.%0A%0A-------------------------------------------------------------------------------%20CONTACT%0AHow%20do%20we%20contact%20you%3F) + +--- + +### Secret generation + +To produce random secrets (`RandomSecret`, `RandomSshSecret`), operator uses [aws-lc-rs+fips](https://docs.rs/aws-lc-rs/latest/aws_lc_rs/). + +To produce deterministic secrets (`DeterministicSecret`, `DeterministicSsh`), operator uses [Hc128](https://docs.rs/rand_hc/latest/rand_hc/) CSPRNG. + +### Secret persistence & memory + +Random secrets defined by `RandomSecret` and `RandomSsh` are unstable. Deletion of generated `Secret` will lead to new random value on next reconcilation. + +Deterministic secrets defined by `DeterministicSecret` and `DeterministicSsh` are stable as long as the base `seed` remains unchanged. +Deletion of generated `Secret` resources will lead to the same pseudo-random value put into the secret on next reconcilation as long as spec remains the same. + +Best effort is taken to ensure that secrets do not remain in memory longer then needed and/or after crush. diff --git a/kuyaseg-controller/src/generator.rs b/kuyaseg-controller/src/generator.rs index f56fb60..00baf8f 100644 --- a/kuyaseg-controller/src/generator.rs +++ b/kuyaseg-controller/src/generator.rs @@ -37,6 +37,7 @@ pub fn str_bytes( ) -> Result, GeneratorErr> { let mut buf = String::with_capacity(length); + // TODO: unique may leak dictionary..? let dictionary = Zeroizing::new(dictionary.unique().collect::>()); let distribution = rand::distr::slice::Choose::new(&dictionary).map_err(|_| GeneratorErr::EmptyDistribution)?; @@ -69,12 +70,12 @@ pub fn str_bytes( } if buf.len() != length { - Err(GeneratorErr::RngStuck { + return Err(GeneratorErr::RngStuck { reason: format!("Unable to sample {length} characters"), - }) - } else { - Ok(buf.into_bytes()) + }); } + + Ok(buf.into_bytes()) } /// Generate SSH keypair, encoded for OpenSSH diff --git a/kuyaseg-controller/src/reporting/k8s_event.rs b/kuyaseg-controller/src/reporting/k8s_event.rs index 5bd1294..80ce3b8 100644 --- a/kuyaseg-controller/src/reporting/k8s_event.rs +++ b/kuyaseg-controller/src/reporting/k8s_event.rs @@ -25,7 +25,7 @@ impl K8sEventRecorder { .recorder .publish(&event.event, &event.object) .await - .inspect_err(|e| tracing::error!(no_event=true, error=?e, "Failed to record K8s event")); + .inspect_err(|e| tracing::error!(k8s_no_event=true, error=?e, "Failed to record K8s event")); } } diff --git a/kuyaseg-controller/src/reporting/mod.rs b/kuyaseg-controller/src/reporting/mod.rs index 240a9c4..d24cbfc 100644 --- a/kuyaseg-controller/src/reporting/mod.rs +++ b/kuyaseg-controller/src/reporting/mod.rs @@ -126,7 +126,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella } queue.push_back(ev); if queue.len() > WARN_THRESHOLD && !warned { - tracing::warn!(no_event=true, queue_len=queue.len(), "K8s event queue exceeded threshold"); + tracing::warn!(k8s_no_event=true, queue_len=queue.len(), "K8s event queue exceeded threshold"); warned = true; } else if queue.len() <= WARN_THRESHOLD { warned = false; @@ -135,7 +135,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella rec_limit = 4; } Err(e) => { - tracing::error!(no_event=true, debug=?e, "Failed to receive k8s event from tracing"); + tracing::error!(k8s_no_event=true, debug=?e, "Failed to receive k8s event from tracing"); rx = rtx.subscribe(); // dumb recover strategy tokio::time::sleep(Duration::from_secs(3)).await; rec_limit -= 1; @@ -180,7 +180,7 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender< rec_limit = 4; } Err(e) => { - tracing::error!(no_pro=true, debug=?e, "Failed to receive prometheus measurement from tracing"); + tracing::error!(k8s_no_pro=true, debug=?e, "Failed to receive prometheus measurement from tracing"); rx = rtx.subscribe(); // dumb recover strategy; tokio::time::sleep(Duration::from_secs(3)).await; rec_limit -= 1; @@ -205,11 +205,11 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender< metrics.register(&mut registry); }, _ = registration_cancellation.cancelled() => { - tracing::warn!(no_pro=true, "Stopped publishing prometheus metrics: consumer job cancelled"); + tracing::warn!(k8s_no_pro=true, "Stopped publishing prometheus metrics: consumer job cancelled"); break; } _ = ctk.cancelled() => { - tracing::warn!(no_pro=true, "Stopped publishing prometheus metrics: shutdown"); + tracing::warn!(k8s_no_pro=true, "Stopped publishing prometheus metrics: shutdown"); break; } } @@ -251,7 +251,7 @@ fn init_otel(joinset: &mut JoinSet<()>, ct: CancellationToken) -> anyhow::Result } Err(e) => { tracing::error!( - no_otel = true, + k8s_no_otel = true, debug = ?e, "Failed to receive otel observation from tracing" ); @@ -263,7 +263,7 @@ fn init_otel(joinset: &mut JoinSet<()>, ct: CancellationToken) -> anyhow::Result } } } - tracing::error!(no_otel = true, "OpenTelemetry reporting from tracing stopped"); + tracing::error!(k8s_no_otel = true, "OpenTelemetry reporting from tracing stopped"); }); Ok(tx) diff --git a/kuyaseg-controller/src/reporting/prometheus.rs b/kuyaseg-controller/src/reporting/prometheus.rs index 4d0d7ba..c648ffb 100644 --- a/kuyaseg-controller/src/reporting/prometheus.rs +++ b/kuyaseg-controller/src/reporting/prometheus.rs @@ -47,6 +47,7 @@ impl ProMetrics { } /// Start measuring reconcile duration. Registers on `TimedMeasurer` drop + // TODO: use this pub fn measure_reconcilation(&self) -> TimedMeasurer { self.reconcile_metrics.measure() } diff --git a/kuyaseg-controller/src/reporting/tracing_evt.rs b/kuyaseg-controller/src/reporting/tracing_evt.rs index 0e1c8fa..7663f3a 100644 --- a/kuyaseg-controller/src/reporting/tracing_evt.rs +++ b/kuyaseg-controller/src/reporting/tracing_evt.rs @@ -302,9 +302,9 @@ impl Visit for SpanFields { fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { match field.name() { - "no_event" => self.no_event = Some(value), - "no_pro" => self.no_pro = Some(value), - "no_otel" => self.no_otel = Some(value), + "k8s_no_event" => self.no_event = Some(value), + "k8s_no_pro" => self.no_pro = Some(value), + "k8s_no_otel" => self.no_otel = Some(value), _ => {} } } @@ -361,9 +361,9 @@ impl Visit for EventFields { fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { match field.name() { "critical" => self.critical = Some(value), - "no_event" => self.no_event = Some(value), - "no_pro" => self.no_pro = Some(value), - "no_otel" => self.no_otel = Some(value), + "k8s_no_event" => self.no_event = Some(value), + "k8s_no_pro" => self.no_pro = Some(value), + "k8s_no_otel" => self.no_otel = Some(value), _ => {} } } diff --git a/kuyaseg-controller/src/resource/generated/secret/mod.rs b/kuyaseg-controller/src/resource/generated/secret/mod.rs index cfb12e3..b919aba 100644 --- a/kuyaseg-controller/src/resource/generated/secret/mod.rs +++ b/kuyaseg-controller/src/resource/generated/secret/mod.rs @@ -52,12 +52,7 @@ impl GeneratedSecretStatus { return false; }; - (stored.0 == spec.version) - && aws_lc_rs::constant_time::verify_slices_are_equal( - stored.1.as_bytes(), - spec.shape.stable_hash().as_bytes(), - ) - .is_ok() + (stored.0 == spec.version) && (stored.1 == spec.shape.stable_hash()) } } @@ -68,7 +63,13 @@ impl GeneratedSecretSpec { /// This method family assumes that upstream resource has `GeneratedSecretStatus` status. /// /// I wrote this because - hell no - I won't write this code twice for both Random and Deterministic secrets! - async fn reconcile_spec(&self, name: &str, namespace: &str, client: kube::Client) -> anyhow::Result<()> + async fn reconcile_spec( + &self, + name: &str, + namespace: &str, + client: kube::Client, + uid: Option<&str>, + ) -> anyhow::Result<()> where T: HasSecretGenerator + Resource @@ -103,20 +104,28 @@ impl GeneratedSecretSpec { .await .context("Update existing Secret")?, None => self - .create_new_from_spec::(name, namespace, client) + .create_new_from_spec::(name, namespace, client, uid) .await - .context("Update existing Secret")?, + .context("Create new Secret")?, } Ok(()) } - async fn create_new_from_spec(&self, name: &str, namespace: &str, client: kube::Client) -> anyhow::Result<()> + async fn create_new_from_spec( + &self, + name: &str, + namespace: &str, + client: kube::Client, + uid: Option<&str>, + ) -> anyhow::Result<()> where T: HasSecretGenerator + Resource + DeserializeOwned, { + use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference; + let self_api = Api::::namespaced(client.clone(), namespace); let target_api = Api::::namespaced(client.clone(), namespace); @@ -127,7 +136,19 @@ impl GeneratedSecretSpec { .collect::>(); // first we try with dry-run, no need to guard as no data here - let partial_target = self.template.to_safe_partial_secret(Some(false), Some("Opaque".into())); + let owner_ref = uid.map(|uid| OwnerReference { + api_version: T::api_version(&()).to_string(), + kind: T::kind(&()).to_string(), + name: name.to_string(), + uid: uid.to_string(), + controller: Some(true), + ..Default::default() + }); + let partial_target = { + let mut secret = self.template.to_safe_partial_secret(Some(false), Some("Opaque".into())); + secret.metadata.owner_references = owner_ref.as_ref().map(|oref| vec![oref.clone()]); + secret + }; let patch = kube::api::Patch::Apply(json!({ "apiVersion": T::api_version(&()), "kind": T::kind(&()), @@ -201,8 +222,7 @@ impl GeneratedSecretSpec { }) .context("Create Secret")?; - // TODO: delete created secret if this fails or not..? - self_api + if let Err(e) = self_api .patch_status( name, &kube::api::PatchParams { @@ -213,8 +233,18 @@ impl GeneratedSecretSpec { }, &patch, ) - .await.inspect_err(|e| tracing::error!(debug=?e, reason="SelfUpdateFailed", action = "CreateNew", "Failed to update self status")) - .context("Update status")?; + .await + .inspect_err(|e| tracing::error!(debug=?e, reason="SelfUpdateFailed", action = "CreateNew", "Failed to update self status")) + .context("Update status") + { + tracing::error!( + reason = "SecretOrphanCleanup", + action = "CreateNew", + "Deleting orphan secret after failed status update" + ); + target_api.delete(name, &kube::api::DeleteParams::default()).await.context("Orphan secret on fail")?; + return Err(e); + } } Ok(()) @@ -344,9 +374,11 @@ impl GeneratedSecretShape { let mut hasher = blake3::Hasher::new(); match self { Self::Bytes { size } => { + hasher.update(b"bytes"); hasher.update(&size.to_le_bytes()); } Self::Password { length, dictionary } => { + hasher.update(b"password"); hasher.update(&length.to_le_bytes()); hasher.update(dictionary.as_bytes()); } diff --git a/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs b/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs index 5df999b..1111bf6 100644 --- a/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs +++ b/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs @@ -58,8 +58,14 @@ impl RandomSecret { let kind = Self::kind(&()); let api = Self::api_version(&()); let name = &self.name_any(); - let _span_nons = - tracing::info_span!("RandomSecret.reconcile", kind = kind.as_ref(), api = api.as_ref(), name).entered(); + let _span_nons = tracing::info_span!( + "RandomSecret.reconcile", + k8s_kind = kind.as_ref(), + k8s_api = api.as_ref(), + k8s_name = name, + k8s_uid = self.uid(), + ) + .entered(); let Some(namespace) = self.namespace() else { tracing::error!( @@ -69,10 +75,11 @@ impl RandomSecret { ); return Err(anyhow::Error::msg("RandomSecret needed namespace")); }; - let _span = tracing::info_span!("RandomSecret.reconcile.ns", ns = namespace).entered(); + let _span = tracing::info_span!("RandomSecret.reconcile.ns", k8s_ns = namespace).entered(); - self.spec.0.reconcile_spec::(name, &namespace, client).await.inspect_err(|e| tracing::error!(debug=?e, reason="RandomSecretReconcilationFailed", action = "Reconcile", "Failed to reconcile RandomSecret {namespace}{name}")).context("Reconcile")?; + self.spec.0.reconcile_spec::(name, &namespace, client, self.uid().as_deref()).await.inspect_err(|e| tracing::error!(debug=?e, reason="RandomSecretReconciliationFailed", action = "Reconcile", "Failed to reconcile RandomSecret {namespace}{name}")).context("Reconcile")?; + // TODO: verify if all desired events fire Ok(Action::await_change()) } } @@ -130,9 +137,10 @@ impl DeterministicSecret { let name = &self.name_any(); let _span_nons = tracing::info_span!( "DeterministicSecret.reconcile", - kind = kind.as_ref(), - api = api.as_ref(), - name + k8s_kind = kind.as_ref(), + k8s_api = api.as_ref(), + k8s_name = name, + k8s_uid = self.uid(), ) .entered(); @@ -144,17 +152,18 @@ impl DeterministicSecret { ); return Err(anyhow::Error::msg("DeterministicSecret needed namespace")); }; - let _span = tracing::info_span!("DeterministicSecret.reconcile.ns", ns = namespace).entered(); + let _span = tracing::info_span!("DeterministicSecret.reconcile.ns", k8s_ns = namespace).entered(); - self.spec.0.reconcile_spec::(name, &namespace, client).await.inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretReconcilationFailed", action = "Reconcile", "Failed to reconcile DeterministicSecret {namespace}{name}")).context("Reconcile")?; + self.spec.0.reconcile_spec::(name, &namespace, client, self.uid().as_deref()).await.inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretReconciliationFailed", action = "Reconcile", "Failed to reconcile DeterministicSecret {namespace}{name}")).context("Reconcile")?; + // TODO: verify if all desired events fire Ok(Action::await_change()) } } impl HasSecretGenerator for DeterministicSecret { fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result> { - let seed = std::env::var("KUYASEG_SEED").inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretNeedSeed", "Refuse to generate deterministic secret without seet. Set with KUYASEG_SEED env")).context("Get external seed")?; + let seed = std::env::var("KUYASEG_SEED").inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretNeedSeed", "Refuse to generate deterministic secret without seed. Set with KUYASEG_SEED env")).context("Get external seed")?; let mut data = BTreeMap::new(); for key in spec.keys.iter() { diff --git a/opencode.jsonc b/opencode.jsonc new file mode 100644 index 0000000..ef7fc30 --- /dev/null +++ b/opencode.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "https://opencode.ai/config.json", + "watcher": { + "ignore": [".opencode/node_modules", ".opencode/package.json", ".opencode/package-lock.json"], + }, + "permission": { + "bash": "ask", + "read": { + ".env": "ask", + ".envrc": "ask", + }, + }, +} From 67a930464adcb3afb7aa3f6d88e281d6c14b89b5 Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 18:42:27 +0500 Subject: [PATCH 04/10] docs: fix yaml --- README.md | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d51daed..a2793e9 100644 --- a/README.md +++ b/README.md @@ -18,23 +18,26 @@ spec: # Specify all keys to generate and put into secret keys: # Binary key - random byte sequence - - kind: binary - # The key to put generated value to - key: binary-key - #If changes after last reconcilation, will force regeneration with new value + - key: binary-key + # Change to regenerate value without altering spec version: 0 - # Set size of value in bytes - size: 64 + # Determine secret shape + shape: !Bytes + # Set size of value in bytes + size: 64 # Password key - guaranteed UTF-8 character sequence with repeat period >= 3 - - kind: password - # The key to put generated value to - key: password-key - #If changes after last reconcilation, will force regeneration with new value + - key: password-key version: 0 - # Set length of password in symbols - length: 24 - # Specify which symbols are allowed to use when generating password - dictionary: "abcdefghijklmnopqrstuvwxyz0123456789_" + shape: !Password + # Set length of password in symbols + length: 24 + # Specify which symbols are allowed to use when generating password + dictionary: "abcdefghijklmnopqrstuvwxyz0123456789_" + # Required fields are `key` and `shape` + - key: default-binary-key + shape: !Bytes + - key: default-password + shape: !Password # Adjust data, stringData and metadata of generated Secret template: {} ``` @@ -108,8 +111,7 @@ metadata: spec: keys: - key: debug-password - kind: password - length: 12 + shape: !Password template: stringData: username: debug-user From 02b46519dccfa375ac156abeda82cef58d3e18bf Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 18:45:06 +0500 Subject: [PATCH 05/10] docs: fix yaml --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2793e9..72d13f5 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,9 @@ Here is a comparison. ||Kuyaseg|kubernetes-secret-generator|ESO|| |-|-|-|-|-| -|Generate cryptographically secure namespaced secrets|Hc128 CSPRNG, allows to specify generated **keys per resource**|Cryptographically secure, allows to specify generated **keys per resource**|Cryptographically secure, but generated keys are specified **per generator**|| +|Generate cryptographically secure namespaced secrets|aws-lc-rs+fips, allows to specify generated **keys per resource**|Cryptographically secure, allows to specify generated **keys per resource**|Cryptographically secure, but generated keys are specified **per generator**|| |Generate SSH keypairs|Ed25519 and RSA OpenSSH keypairs, private and public keys **stored in separate secrets**|Both secret and public keys are **stored in one secret**|Not supported|| -|Deterministic, reproducible secret generation|Available, optional, based on provided secret data (e.g. SealedSecrets). Stable across clusters|Not supported|Not supported|| +|Deterministic, reproducible secret generation|Available, Hc128 CSPRNG, optional, based on provided seed and secret specs. Stable|Not supported|Not supported|| |Secret value stability|Created secrets are
  • immutable
  • created if accidentally deleted
  • don't change unles you ask for it
|Stable across reconcilations, but **changes on secret re-creation**|Only with real **external secret storage** (which ESO is designed for)|| |Target secret template|Respects almost full `Secret` (v1) spec, including
  • annotations
  • labels
  • owner_references
|[Not supported](https://github.com/mittwald/kubernetes-secret-generator/issues/88)|Allows to specity template in `ExternalSecret`|| From 53c25f81a4729be4ad735838756a56c42d1bdcd5 Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 18:47:36 +0500 Subject: [PATCH 06/10] docs: fix yaml --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 72d13f5..ca54d06 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,7 @@ Here is a comparison. |Generate cryptographically secure namespaced secrets|aws-lc-rs+fips, allows to specify generated **keys per resource**|Cryptographically secure, allows to specify generated **keys per resource**|Cryptographically secure, but generated keys are specified **per generator**|| |Generate SSH keypairs|Ed25519 and RSA OpenSSH keypairs, private and public keys **stored in separate secrets**|Both secret and public keys are **stored in one secret**|Not supported|| |Deterministic, reproducible secret generation|Available, Hc128 CSPRNG, optional, based on provided seed and secret specs. Stable|Not supported|Not supported|| -|Secret value stability|Created secrets are
  • immutable
  • created if accidentally deleted
  • don't change unles you ask for it
|Stable across reconcilations, but **changes on secret re-creation**|Only with real **external secret storage** (which ESO is designed for)|| -|Target secret template|Respects almost full `Secret` (v1) spec, including
  • annotations
  • labels
  • owner_references
|[Not supported](https://github.com/mittwald/kubernetes-secret-generator/issues/88)|Allows to specity template in `ExternalSecret`|| +|Target secret template|Respects
  • annotations
  • labels
|[Not supported](https://github.com/mittwald/kubernetes-secret-generator/issues/88)|Allows to specity template in `ExternalSecret`|| ## Security From 33bf3cbc20282fdcd83156d4abdc259a7a3a9e3c Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 16 May 2026 18:49:52 +0500 Subject: [PATCH 07/10] docs: fix yaml --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ca54d06..da2a5bc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # K8s secret generator +> 🚧 Please, if you encountered this repo, note that this is under heavy construction. + > *AI*: This was also an exploration of opencode features, so there is some amount of AI-generated boilerplate. ## Features From e7f6ddf741d8526b0f0db85075ec41cc46a66690 Mon Sep 17 00:00:00 2001 From: spushk Date: Sat, 30 May 2026 10:19:53 +0500 Subject: [PATCH 08/10] fix(reporting)!: drop OTEL (for now) & rename layer field names & fix prometheus registry --- .../src/reporting/{k8s_event.rs => k8s.rs} | 2 +- kuyaseg-controller/src/reporting/mod.rs | 134 +++++------------- kuyaseg-controller/src/reporting/otel.rs | 106 -------------- .../src/reporting/prometheus.rs | 9 +- .../src/reporting/tracing_evt.rs | 114 +++++---------- 5 files changed, 76 insertions(+), 289 deletions(-) rename kuyaseg-controller/src/reporting/{k8s_event.rs => k8s.rs} (86%) delete mode 100644 kuyaseg-controller/src/reporting/otel.rs diff --git a/kuyaseg-controller/src/reporting/k8s_event.rs b/kuyaseg-controller/src/reporting/k8s.rs similarity index 86% rename from kuyaseg-controller/src/reporting/k8s_event.rs rename to kuyaseg-controller/src/reporting/k8s.rs index 80ce3b8..8254852 100644 --- a/kuyaseg-controller/src/reporting/k8s_event.rs +++ b/kuyaseg-controller/src/reporting/k8s.rs @@ -25,7 +25,7 @@ impl K8sEventRecorder { .recorder .publish(&event.event, &event.object) .await - .inspect_err(|e| tracing::error!(k8s_no_event=true, error=?e, "Failed to record K8s event")); + .inspect_err(|e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to record K8s event")); } } diff --git a/kuyaseg-controller/src/reporting/mod.rs b/kuyaseg-controller/src/reporting/mod.rs index d24cbfc..7e352a8 100644 --- a/kuyaseg-controller/src/reporting/mod.rs +++ b/kuyaseg-controller/src/reporting/mod.rs @@ -4,6 +4,7 @@ use std::time::Duration; use anyhow::Context; use confique::Config; +use opentelemetry::TraceId; use tokio::sync::broadcast::{ self, Sender, @@ -11,19 +12,27 @@ use tokio::sync::broadcast::{ use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; -use crate::reporting::k8s_event::K8sEvent; -use crate::reporting::otel::OtelObservation; +use crate::reporting::k8s::K8sEvent; use crate::reporting::prometheus::ProLabel; use crate::reporting::tracing_evt::{ TracingK8sEventLayer, TracingK8sEventLayerConfig, }; -pub mod k8s_event; -pub mod otel; +pub mod k8s; pub mod prometheus; pub mod tracing_evt; +pub fn span_trace_id() -> Option { + use opentelemetry::trace::TraceContextExt; + use tracing_opentelemetry::OpenTelemetrySpanExt; + + match tracing::Span::current().context().span().span_context().trace_id() { + TraceId::INVALID => None, + other => Some(other), + } +} + #[derive(Config)] pub struct ReportingConfig { #[config(env = "KUYASEG_REPORT_K8S")] @@ -32,22 +41,13 @@ pub struct ReportingConfig { #[config(env = "KUYASEG_REPORT_PROMETHEUS")] #[config(default = false)] pub prometheus: bool, - #[config(env = "KUYASEG_REPORT_OTEL")] - #[config(default = false)] - pub opentelemetry: bool, -} - -pub fn trace_id_str(trace_id: &opentelemetry::TraceId) -> Option { - match trace_id { - &opentelemetry::TraceId::INVALID => None, - x => Some(x.to_string()), - } } /// You should await on handle somewhere pub struct ReportingInit { pub tracing_layer: TracingK8sEventLayer, pub handle: JoinSet<()>, + pub pro_registry: Option, } pub fn init(k8s_client: kube::Client, ct: CancellationToken) -> anyhow::Result { @@ -61,21 +61,18 @@ pub fn init(k8s_client: kube::Client, ct: CancellationToken) -> anyhow::Result, ct: Cancella Reporter, }; - use crate::reporting::k8s_event::K8sEventRecorder; + use crate::reporting::k8s::K8sEventRecorder; const WARN_THRESHOLD: usize = 10; const MAX_CAPACITY: usize = 100; @@ -126,7 +123,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella } queue.push_back(ev); if queue.len() > WARN_THRESHOLD && !warned { - tracing::warn!(k8s_no_event=true, queue_len=queue.len(), "K8s event queue exceeded threshold"); + tracing::warn!(k13t.no_report=true, debug=?queue.len(), "K8s event queue exceeded threshold"); warned = true; } else if queue.len() <= WARN_THRESHOLD { warned = false; @@ -135,7 +132,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella rec_limit = 4; } Err(e) => { - tracing::error!(k8s_no_event=true, debug=?e, "Failed to receive k8s event from tracing"); + tracing::error!(k13t.no_report=true, debug=?e, "Failed to receive k8s event from tracing"); rx = rtx.subscribe(); // dumb recover strategy tokio::time::sleep(Duration::from_secs(3)).await; rec_limit -= 1; @@ -145,7 +142,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella } } tracing::error!( - no_event = true, + k13t.no_report = true, "K8s Event reporting from tracing stopped" ); }); @@ -153,7 +150,10 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella tx } -fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender> { +fn init_pro_metrics( + joinset: &mut JoinSet<()>, + ct: CancellationToken, +) -> (Sender>, prometheus_client::registry::Registry) { use prometheus_client::registry::Registry; use crate::reporting::prometheus::ProMetrics; @@ -161,10 +161,7 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender< let (tx, mut rx) = broadcast::channel::>(512); let metrics = ProMetrics::new(); - let registration_cancellation = CancellationToken::new(); - let rtx = tx.clone(); - let ctk = registration_cancellation.clone(); let promt = metrics.clone(); let consumer_ct = ct.clone(); joinset.spawn(async move { @@ -180,7 +177,7 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender< rec_limit = 4; } Err(e) => { - tracing::error!(k8s_no_pro=true, debug=?e, "Failed to receive prometheus measurement from tracing"); + tracing::error!(k13t.no_report=true, debug=?e, "Failed to receive prometheus measurement from tracing"); rx = rtx.subscribe(); // dumb recover strategy; tokio::time::sleep(Duration::from_secs(3)).await; rec_limit -= 1; @@ -189,82 +186,17 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender< } } } - ctk.cancel(); tracing::error!( - no_pro = true, + k13t.no_report=true, "Prometheus measurement from tracing stopped" ); }); - let ctk = ct.clone(); - joinset.spawn(async move { + let registry = { let mut registry = Registry::default(); - loop { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(10)) => { - metrics.register(&mut registry); - }, - _ = registration_cancellation.cancelled() => { - tracing::warn!(k8s_no_pro=true, "Stopped publishing prometheus metrics: consumer job cancelled"); - break; - } - _ = ctk.cancelled() => { - tracing::warn!(k8s_no_pro=true, "Stopped publishing prometheus metrics: shutdown"); - break; - } - } - } - }); + metrics.register(&mut registry); + registry + }; - tx -} - -fn init_otel(joinset: &mut JoinSet<()>, ct: CancellationToken) -> anyhow::Result>> { - use crate::reporting::otel::OtelAggregator; - - let exporter = opentelemetry_otlp::LogExporter::builder() - .with_tonic() - .build() - .context("Failed to build OTLP log exporter")?; - - let provider = opentelemetry_sdk::logs::SdkLoggerProvider::builder() - .with_batch_exporter(exporter) - .build(); - - let aggregator = Arc::new(OtelAggregator::new(provider)); - let task_aggregator = aggregator.clone(); - - let (tx, mut rx) = broadcast::channel::>(512); - - let rtx = tx.clone(); - joinset.spawn(async move { - let mut rec_limit = 4; - while rec_limit > 0 { - tokio::select! { - biased; - _ = ct.cancelled() => break, - msg = rx.recv() => { - match msg { - Ok(obs) => { - task_aggregator.observe(&obs); - rec_limit = 4; - } - Err(e) => { - tracing::error!( - k8s_no_otel = true, - debug = ?e, - "Failed to receive otel observation from tracing" - ); - rx = rtx.subscribe(); - tokio::time::sleep(Duration::from_secs(3)).await; - rec_limit -= 1; - } - } - } - } - } - tracing::error!(k8s_no_otel = true, "OpenTelemetry reporting from tracing stopped"); - }); - - Ok(tx) + (tx, registry) } diff --git a/kuyaseg-controller/src/reporting/otel.rs b/kuyaseg-controller/src/reporting/otel.rs deleted file mode 100644 index f850d4c..0000000 --- a/kuyaseg-controller/src/reporting/otel.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::fmt::Debug; - -use opentelemetry::logs::{ - AnyValue, - LogRecord, - Logger, - LoggerProvider, - Severity, -}; -use opentelemetry_sdk::logs::SdkLoggerProvider; - -pub struct OtelLogFields { - pub severity: Severity, - pub message: String, - pub reason: String, - pub action: String, - pub k8s_name: String, - pub k8s_kind: String, - pub k8s_ns: Option, - pub k8s_api: Option, - pub k8s_uid: Option, - pub trace_id: Option, -} - -pub enum OtelObservation { - Log { fields: OtelLogFields }, - Metric {}, -} - -impl Debug for OtelObservation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let variant = match self { - Self::Log { .. } => "Log", - Self::Metric { .. } => "Metric", - }; - write!(f, "OtelObservation::{}", variant) - } -} - -pub struct OtelAggregator { - provider: SdkLoggerProvider, - logger: ::Logger, -} - -impl OtelAggregator { - pub fn new(provider: SdkLoggerProvider) -> Self { - let logger = provider.logger("kuyaseg-controller"); - Self { provider, logger } - } - - pub fn observe(&self, obs: &OtelObservation) { - match obs { - OtelObservation::Log { fields } => self.observe_log(fields), - OtelObservation::Metric {} => { /*Not implemented */ } - } - } - - pub fn force_flush(&self) { - if let Err(e) = self.provider.force_flush() { - tracing::warn!(error = ?e, "otel export force_flush failed"); - } - } - - fn observe_log(&self, fields: &OtelLogFields) { - let record = { - let mut record = self.logger.create_log_record(); - record.set_severity_number(fields.severity); - record.set_severity_text(fields.severity.name()); - record.set_body(AnyValue::from(fields.message.to_string())); - record.set_target("kuyaseg-controller"); - - record.add_attribute("reason", fields.reason.to_string()); - record.add_attribute("action", fields.action.to_string()); - record.add_attribute("k8s.resource.name", fields.k8s_name.to_string()); - record.add_attribute("k8s.resource.kind", fields.k8s_kind.to_string()); - - if let Some(ns) = fields.k8s_ns.as_ref() { - record.add_attribute("k8s.namespace.name", ns.to_string()); - } - if let Some(api) = fields.k8s_api.as_ref() { - record.add_attribute("k8s.resource.api_version", api.to_string()); - } - if let Some(uid) = fields.k8s_uid.as_ref() { - record.add_attribute("k8s.resource.uid", uid.to_string()); - } - if let Some(trace_id) = fields.trace_id.as_ref() { - record.add_attribute("trace_id", trace_id.to_string()); - } - - record - }; - - self.logger.emit(record); - } - - #[deprecated(note = "Not implemented")] - fn observe_metric(&self, _metric: ()) { - todo!("Metrics are not implemented and should not be used for now") - } -} - -impl Drop for OtelAggregator { - fn drop(&mut self) { - self.force_flush(); - } -} diff --git a/kuyaseg-controller/src/reporting/prometheus.rs b/kuyaseg-controller/src/reporting/prometheus.rs index c648ffb..dfcc3b1 100644 --- a/kuyaseg-controller/src/reporting/prometheus.rs +++ b/kuyaseg-controller/src/reporting/prometheus.rs @@ -4,7 +4,6 @@ use std::time::{ SystemTime, }; -use opentelemetry::trace::TraceContextExt; use prometheus_client::encoding::{ EncodeLabelSet, EncodeLabelValue, @@ -16,9 +15,8 @@ use prometheus_client::metrics::exemplar::{ }; use prometheus_client::metrics::family::Family; use prometheus_client::registry::Registry; -use tracing_opentelemetry::OpenTelemetrySpanExt; -use crate::reporting::trace_id_str; +use crate::reporting::span_trace_id; #[derive(Default, Clone)] pub struct ProMetrics { @@ -72,8 +70,9 @@ pub struct ProReconcileMetrics { impl ProReconcileMetrics { pub fn measure(&self) -> TimedMeasurer { self.runs.inc(); - let trace_id = tracing::Span::current().context().span().span_context().trace_id(); - let label = trace_id_str(&trace_id).map(|trace_id| TraceLabel { trace_id }); + let label = span_trace_id().map(|id| TraceLabel { + trace_id: id.to_string(), + }); TimedMeasurer { label, diff --git a/kuyaseg-controller/src/reporting/tracing_evt.rs b/kuyaseg-controller/src/reporting/tracing_evt.rs index 7663f3a..f94b612 100644 --- a/kuyaseg-controller/src/reporting/tracing_evt.rs +++ b/kuyaseg-controller/src/reporting/tracing_evt.rs @@ -5,7 +5,6 @@ use kube::runtime::events::{ Event, EventType, }; -use opentelemetry::logs::Severity; use tokio::sync::broadcast::Sender; use tracing::field::Visit; use tracing::span::Attributes; @@ -14,31 +13,23 @@ use tracing::{ Id, Subscriber, }; -use tracing_opentelemetry::OtelData; use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; -use crate::reporting::k8s_event::K8sEvent; -use crate::reporting::otel::{ - OtelLogFields, - OtelObservation, -}; +use crate::reporting::k8s::K8sEvent; use crate::reporting::prometheus::{ ProK8sExemplar, ProK8sLabel, ProK8sSeverity, ProLabel, }; -use crate::reporting::trace_id_str; pub struct TracingK8sEventLayerConfig { /// Set to enable k8s events from tracing pub k8s_events: Option>>, /// set to enable prometheus metrics from tracing pub prometheus: Option>>, - /// set to enable otel observations from tracing - pub otel: Option>>, } /// Watches for tracing events, containing information necessary to construst k8s events and write prometheus metrics. @@ -46,10 +37,11 @@ pub struct TracingK8sEventLayerConfig { /// - k8s native events you must record yourself - thats what `tx` is for. /// - prometheus metrics are written automatically (if metrics set), but labels are also send with `tx`. /// - k8s events are rate-limited to 10 events/min +/// +/// Recognizes fields with `k13t` (KubernatesEvent) prefix. pub struct TracingK8sEventLayer { k8s_tx: Option>>, pro_tx: Option>>, - otel_tx: Option>>, } impl TracingK8sEventLayer { @@ -57,7 +49,6 @@ impl TracingK8sEventLayer { Self { k8s_tx: config.k8s_events, pro_tx: config.prometheus, - otel_tx: config.otel, } } } @@ -90,6 +81,7 @@ where return; } + let span = ctx.event_span(event); let scope = match ctx.event_scope(event) { Some(s) => s, None => { @@ -122,11 +114,13 @@ where return; } - // gather trace_id - let otel_trace_id = ctx - .event_span(event) - .and_then(|span| span.extensions().get::().and_then(|dat| dat.trace_id())); - let str_trace_id = otel_trace_id.as_ref().and_then(trace_id_str); + let trace_id = span + .and_then(|s| { + s.extensions() + .get::() + .map(|ctx| ctx.trace_id()) + }) + .map(|id| id.to_string()); // write k8s event let k8s_event_type = match *event.metadata().level() { @@ -145,7 +139,7 @@ where object: span_fields.to_object(), event, })) - .inspect_err(|e| tracing::error!(debug=?e, "Failed to issue event on k8s event from tracing")); + .inspect_err(|e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to issue event on k8s event from tracing")); } // write/measure pro event @@ -163,48 +157,13 @@ where }; if event_fields.no_pro.is_none_or(|v| !v) && span_fields.no_pro.is_none_or(|v| !v) - && let Some(label) = try_create_pro_label(&span_fields, &event_fields, pro_severity, str_trace_id.clone()) + && let Some(label) = try_create_pro_label(&span_fields, &event_fields, pro_severity, trace_id) && let Some(tx) = self.pro_tx.as_ref() { _ = tx.send(Arc::new(label)).inspect_err( - |e| tracing::error!(debug=?e, "Failed to issue event on prometheus label measure from tracing"), + |e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to issue event on prometheus label measure from tracing"), ); } - - // write otel event - let otel_severity = match *event.metadata().level() { - tracing::Level::ERROR => { - if event_fields.critical.is_some_and(|v| v) { - Severity::Fatal - } else { - Severity::Error - } - } - tracing::Level::WARN => Severity::Warn, - tracing::Level::INFO => Severity::Info, - _ => return, - }; - if event_fields.no_otel.is_none_or(|v| !v) - && span_fields.no_otel.is_none_or(|v| !v) - && let Some(tx) = self.otel_tx.as_ref() - { - _ = tx - .send(Arc::new(OtelObservation::Log { - fields: OtelLogFields { - severity: otel_severity, - message: event_fields.message.clone().unwrap_or_default(), - reason: event_fields.reason.clone().unwrap_or_default(), - action: event_fields.action.clone().unwrap_or_default(), - k8s_name: span_fields.name.clone().unwrap_or_default(), - k8s_kind: span_fields.kind.clone().unwrap_or_default(), - k8s_ns: span_fields.ns.clone(), - k8s_api: span_fields.api.clone(), - k8s_uid: span_fields.uid.clone(), - trace_id: str_trace_id.clone(), - }, - })) - .inspect_err(|e| tracing::error!(debug=?e, "Failed to issue otel observation from tracing")); - } } } @@ -289,22 +248,22 @@ impl SpanFields { impl Visit for SpanFields { fn record_str(&mut self, field: &tracing::field::Field, value: &str) { match field.name() { - "k8s_name" => self.name = Some(value.to_string()), - "k8s_ns" => self.ns = Some(value.to_string()), - "k8s_kind" => self.kind = Some(value.to_string()), - "k8s_api" => self.api = Some(value.to_string()), - "k8s_uid" => self.uid = Some(value.to_string()), - "k8s_version" => self.version = Some(value.to_string()), - "k8s_field_path" => self.field_path = Some(value.to_string()), + "k13t.name" => self.name = Some(value.to_string()), + "k13t.ns" => self.ns = Some(value.to_string()), + "k13t.kind" => self.kind = Some(value.to_string()), + "k13t.api" => self.api = Some(value.to_string()), + "k13t.uid" => self.uid = Some(value.to_string()), + "k13t.version" => self.version = Some(value.to_string()), + "k13t.field_path" => self.field_path = Some(value.to_string()), _ => {} } } fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { match field.name() { - "k8s_no_event" => self.no_event = Some(value), - "k8s_no_pro" => self.no_pro = Some(value), - "k8s_no_otel" => self.no_otel = Some(value), + "k13t.no_event" => self.no_event = Some(value), + "k13t.no_pro" => self.no_pro = Some(value), + "k13t.no_otel" => self.no_otel = Some(value), _ => {} } } @@ -314,12 +273,14 @@ impl Visit for SpanFields { #[derive(Default, Clone)] struct EventFields { - /// Setting this to true will block entire span from emitting k8s events + /// Setting this to true will prevent from emitting k8s events no_event: Option, - /// Setting this to true will block entire span from emitting prometheus metric labels + /// Setting this to true will prevent from emitting prometheus metric labels no_pro: Option, - /// Setting this to true will block entire span from emitting opentelemetry logs, + /// Setting this to true will prevent from emitting opentelemetry logs, no_otel: Option, + /// Setting this to true will prevent from emitting any pro/otel/k8s events + no_report: Option, /// Only for: /// - prometheus label: force event severity to "critical". Only triggers on `ERROR` tracing level /// - opentelemetry: make event severify to "Fatal". Only triggers on `ERROR` tracing level @@ -332,7 +293,7 @@ struct EventFields { impl EventFields { /// Scope independend custom field validation fn check_fields(&self) -> bool { - self.reason.is_some() && self.action.is_some() + !self.no_report.is_some_and(|v| v) && self.reason.is_some() && self.action.is_some() } /// Return `None` if hard-requirements on fields not met. @@ -351,19 +312,20 @@ impl EventFields { impl Visit for EventFields { fn record_str(&mut self, field: &tracing::field::Field, value: &str) { match field.name() { - "reason" => self.reason = Some(value.to_string()), - "action" => self.action = Some(value.to_string()), - "message" => self.message = Some(value.to_string()), + "k13t.reason" => self.reason = Some(value.to_string()), + "k13t.action" => self.action = Some(value.to_string()), + "k13t.message" => self.message = Some(value.to_string()), _ => {} } } fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { match field.name() { - "critical" => self.critical = Some(value), - "k8s_no_event" => self.no_event = Some(value), - "k8s_no_pro" => self.no_pro = Some(value), - "k8s_no_otel" => self.no_otel = Some(value), + "k13t.critical" => self.critical = Some(value), + "k13t.no_event" => self.no_event = Some(value), + "k13t.no_pro" => self.no_pro = Some(value), + "k13t.no_otel" => self.no_otel = Some(value), + "k13t.no_report" => self.no_report = Some(value), _ => {} } } From 3ac37809b7130a50c55505758fcf33abc13b76db Mon Sep 17 00:00:00 2001 From: spushk Date: Tue, 2 Jun 2026 06:10:46 +0500 Subject: [PATCH 09/10] chore!: minor fixes here and there & move CRDs into submods & introduce serializer helpers --- Cargo.lock | 1028 +++++------------ Cargo.toml | 16 +- README.md | 29 +- kuyaseg-controller/Cargo.toml | 7 +- kuyaseg-controller/src/bin/crdgen.rs | 3 + kuyaseg-controller/src/bin/generate_crds.rs | 14 - kuyaseg-controller/src/generator.rs | 2 - kuyaseg-controller/src/reporting/mod.rs | 1 + .../src/resource/generated/secret/mod.rs | 608 ---------- .../src/resource/generated/secret/v1alpha1.rs | 191 --- .../resource/generated/secret/v1alpha1/mod.rs | 354 ++++++ .../generated/secret/v1alpha1/pseudorandom.rs | 0 .../generated/secret/v1alpha1/random.rs | 0 .../src/resource/generated/ssh/mod.rs | 2 +- kuyaseg-controller/src/utils/mod.rs | 1 + kuyaseg-controller/src/utils/serializers.rs | 106 ++ 16 files changed, 815 insertions(+), 1547 deletions(-) create mode 100644 kuyaseg-controller/src/bin/crdgen.rs delete mode 100644 kuyaseg-controller/src/bin/generate_crds.rs delete mode 100644 kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs create mode 100644 kuyaseg-controller/src/resource/generated/secret/v1alpha1/mod.rs create mode 100644 kuyaseg-controller/src/resource/generated/secret/v1alpha1/pseudorandom.rs create mode 100644 kuyaseg-controller/src/resource/generated/secret/v1alpha1/random.rs create mode 100644 kuyaseg-controller/src/utils/serializers.rs diff --git a/Cargo.lock b/Cargo.lock index 2d22c33..6aaf74c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,15 +8,15 @@ version = "0.6.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" dependencies = [ - "crypto-common 0.2.1", + "crypto-common 0.2.2", "inout", ] [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ "cipher", "cpubits", @@ -26,9 +26,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0-rc.3" +version = "0.11.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" +checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" dependencies = [ "aead", "aes", @@ -67,6 +67,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -122,17 +131,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -141,9 +139,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-fips-sys" @@ -281,7 +279,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn", ] @@ -303,6 +301,7 @@ dependencies = [ "cfg-if", "constant_time_eq", "cpufeatures 0.3.0", + "serde", "zeroize", ] @@ -326,10 +325,19 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.20.2" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" @@ -339,14 +347,14 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -364,12 +372,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chacha20" version = "0.10.0" @@ -384,13 +386,25 @@ dependencies = [ ] [[package]] -name = "cipher" -version = "0.5.1" +name = "chrono" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer 0.12.0", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "inout", "zeroize", ] @@ -417,9 +431,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "concurrent-queue" @@ -516,18 +530,18 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] [[package]] name = "ctr" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ "cipher", ] @@ -543,12 +557,12 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "5.0.0-pre.6" +version = "5.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" +checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", @@ -601,6 +615,16 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -648,18 +672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", - "crypto-common 0.2.1", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "crypto-common 0.2.2", ] [[package]] @@ -691,9 +704,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-pre.7" +version = "3.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6" +checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" dependencies = [ "curve25519-dalek", "ed25519", @@ -715,9 +728,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "enum-ordinalize" @@ -794,12 +807,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -931,10 +938,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -944,11 +949,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -993,23 +996,10 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.4.14" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" @@ -1043,6 +1033,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hostname" version = "0.4.2" @@ -1056,9 +1052,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1111,15 +1107,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -1167,16 +1162,13 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1185,85 +1177,27 @@ dependencies = [ ] [[package]] -name = "icu_collections" -version = "2.2.0" +name = "iana-time-zone" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", ] [[package]] -name = "icu_locale_core" -version = "2.2.0" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "cc", ] [[package]] @@ -1279,24 +1213,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] -name = "idna" -version = "1.1.0" +name = "indexmap" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", + "autocfg", + "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1320,12 +1244,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - [[package]] name = "itertools" version = "0.13.0" @@ -1352,9 +1270,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1362,14 +1280,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -1403,9 +1321,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1456,7 +1374,7 @@ checksum = "51b326f5219dd55872a72c1b6ddd1b830b8334996c667449c29391d657d78d5e" dependencies = [ "base64", "jiff", - "schemars", + "schemars 1.2.1", "serde", "serde_json", ] @@ -1521,7 +1439,7 @@ dependencies = [ "jiff", "json-patch", "k8s-openapi", - "schemars", + "schemars 1.2.1", "serde", "serde-value", "serde_json", @@ -1585,15 +1503,15 @@ dependencies = [ "kube", "mimalloc", "opentelemetry", - "opentelemetry-otlp", "opentelemetry_sdk", "prometheus-client", "rand 0.10.1", "rand_hc", - "schemars", + "schemars 1.2.1", "scopeguard", "serde", "serde_json", + "serde_with", "serde_yaml", "smallvec", "ssh-key", @@ -1636,19 +1554,13 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.47" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", ] -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - [[package]] name = "lock_api" version = "0.4.14" @@ -1660,15 +1572,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "matchers" @@ -1687,15 +1593,15 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mimalloc" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -1714,9 +1620,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1742,6 +1648,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -1765,9 +1677,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -1777,66 +1689,20 @@ dependencies = [ "tracing", ] -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" -dependencies = [ - "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest", - "thiserror", - "tokio", - "tonic", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", - "tonic-prost", -] - [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.4", "thiserror", - "tokio", - "tokio-stream", ] [[package]] @@ -2009,13 +1875,10 @@ dependencies = [ ] [[package]] -name = "potential_utf" -version = "0.1.5" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -2068,84 +1931,6 @@ dependencies = [ "syn", ] -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -2280,46 +2065,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "ring" version = "0.17.14" @@ -2366,9 +2111,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2382,7 +2127,6 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] @@ -2418,6 +2162,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.2.1" @@ -2550,9 +2306,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2584,13 +2340,45 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -2634,6 +2422,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2664,9 +2458,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2674,9 +2468,9 @@ dependencies = [ [[package]] name = "ssh-cipher" -version = "0.3.0-rc.9" +version = "0.3.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" +checksum = "68126a396f08c64551b44cdb65865ef1d78fdf16ac02b53f78588227f60466fe" dependencies = [ "aes", "aes-gcm", @@ -2690,9 +2484,9 @@ dependencies = [ [[package]] name = "ssh-encoding" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" dependencies = [ "base64ct", "ctutils", @@ -2718,12 +2512,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "strsim" version = "0.11.1" @@ -2752,20 +2540,6 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "thiserror" @@ -2797,13 +2571,34 @@ dependencies = [ ] [[package]] -name = "tinystr" -version = "0.8.3" +name = "time" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ - "displaydoc", - "zerovec", + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", ] [[package]] @@ -2827,7 +2622,6 @@ version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "bytes", "libc", "mio", "pin-project-lite", @@ -2858,17 +2652,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -2883,43 +2666,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tonic" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" -dependencies = [ - "async-trait", - "base64", - "bytes", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" -dependencies = [ - "bytes", - "prost", - "tonic", -] - [[package]] name = "tower" version = "0.5.3" @@ -2928,9 +2674,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", "pin-project-lite", - "slab", "sync_wrapper", "tokio", "tokio-util", @@ -2941,14 +2685,13 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "base64", "bitflags", "bytes", - "futures-util", "http", "http-body", "mime", @@ -2957,7 +2700,6 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "url", ] [[package]] @@ -3018,9 +2760,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", @@ -3058,9 +2800,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3086,7 +2828,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] @@ -3108,24 +2850,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "valuable" version = "0.1.1" @@ -3173,9 +2897,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3184,21 +2908,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3206,9 +2920,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3219,9 +2933,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -3243,7 +2957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -3256,20 +2970,10 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] -[[package]] -name = "web-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -3280,28 +2984,72 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3319,31 +3067,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3352,96 +3083,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3476,7 +3159,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -3507,7 +3190,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -3526,7 +3209,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -3536,76 +3219,26 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.2" @@ -3626,39 +3259,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 9015c63..a64b8e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,15 +22,12 @@ tokio-util.version = '^0.7' axum.version = "^0.8" ## Tracing tracing.version = "^0.1" -tracing.features = ["max_level_trace", "release_max_level_info"] +tracing.features = ["max_level_debug", "release_max_level_info"] tracing-subscriber.version = "^0.3" -tracing-opentelemetry.version = "^0.32" -opentelemetry.version = "^0.31" -opentelemetry.features = ["trace"] -opentelemetry-otlp.version = "^0.31" -opentelemetry-otlp.features = ["grpc-tonic", "reqwest-rustls"] -opentelemetry_sdk.version = "^0.31" -opentelemetry_sdk.features = ["rt-tokio"] +tracing-opentelemetry.version = "^0.33" +opentelemetry.version = "^0.32" +opentelemetry_sdk.version = "^0.32" +prometheus-client.version = "^0.24" ## Utils jiff.version = "^0.2" scopeguard.version = "^1" @@ -42,6 +39,8 @@ schemars.version = "^1" base64.version = "^0.22" serde.version = "^1" serde.features = ["derive"] +serde_with.version = "3" +serde_with.features = ["hex", "schemars_1"] serde_json.version = "^1" serde_yaml.version = "^0.9" ## k8s @@ -49,7 +48,6 @@ kube.version = "^3" kube.features = ["derive"] k8s-openapi.version = "^0.27" k8s-openapi.features = ["latest", "schemars"] -prometheus-client.version = "^0.24" ## Codegen thiserror.version = "^2" anyhow.version = "^1" diff --git a/README.md b/README.md index da2a5bc..2d2df58 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ Both resources inherit namespace and name (as prefix) from `RandomSsh` resource > ⚠️ This feature exists primarily for testing purposes, where security is less of a concern then repeatability. > -> While there are use-cases for predictable secrets in production, you should consider [ESO](https://external-secrets.io) +> While there are use-cases for predictable secrets in production (e.g. DB credentials in some cases), +> you should consider [ESO](https://external-secrets.io) > with secure external storage to share secrets between clusters and keep secrets stable. > > If the seed secret gets compromised - every past and future generated secret can be easliy computed. @@ -126,6 +127,24 @@ Secret values generated by deterministic generator depend on: Thus, if seed secret remains stable and DeterministicSecret specification does not change - generated keys remain stable and predictable +To set the `seed` for the controller, deploy a `v1/Secret` and target the controller to it with `KUYASEG_DETERMINISTIC_SEED` env variable. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: controller-seed + namespace: testing +stringData: + my_seed: aabbcc123 +``` + +`KUYASEG_DETERMINISTIC_SEED=controller-seed/my_seed` + +When the later part `/my_seed` is omitted, controller will expect the `seed` key to contain seed value. + +When the seed *value* changes, all known secrets are reconciled. + ## Feature comparison We know there are plenty of decent, mature and well-adopted solutions like the one by [mittwald](https://github.com/mittwald/kubernetes-secret-generator) and even [ESO](https://external-secrets.io) which can also generate secrets. @@ -156,11 +175,11 @@ To produce random secrets (`RandomSecret`, `RandomSshSecret`), operator uses [aw To produce deterministic secrets (`DeterministicSecret`, `DeterministicSsh`), operator uses [Hc128](https://docs.rs/rand_hc/latest/rand_hc/) CSPRNG. -### Secret persistence & memory +### Secret persistence -Random secrets defined by `RandomSecret` and `RandomSsh` are unstable. Deletion of generated `Secret` will lead to new random value on next reconcilation. +Random secrets defined by `RandomSecret` and `RandomSsh` are unstable: deletion of generated `Secret` will produce new random value on next reconcilation. -Deterministic secrets defined by `DeterministicSecret` and `DeterministicSsh` are stable as long as the base `seed` remains unchanged. -Deletion of generated `Secret` resources will lead to the same pseudo-random value put into the secret on next reconcilation as long as spec remains the same. +Deterministic secrets defined by `DeterministicSecret` and `DeterministicSsh` are stable as long as the base `seed` and spec/version remain unchanged. +Deletion of generated `Secret` resources will lead to the same pseudo-random value put into the secret on next reconcilation. Best effort is taken to ensure that secrets do not remain in memory longer then needed and/or after crush. diff --git a/kuyaseg-controller/Cargo.toml b/kuyaseg-controller/Cargo.toml index 5435d05..8affe9a 100644 --- a/kuyaseg-controller/Cargo.toml +++ b/kuyaseg-controller/Cargo.toml @@ -11,8 +11,8 @@ publish.workspace = true [[bin]] doc = false -name = "generate_crds" -path = "src/bin/generate_crds.rs" +name = "crdgen" +path = "src/bin/crdgen.rs" [dependencies] ## Rt @@ -26,7 +26,6 @@ tracing-subscriber.workspace = true tracing-subscriber.features = ["env-filter"] tracing-opentelemetry.workspace = true opentelemetry.workspace = true -opentelemetry-otlp.workspace = true opentelemetry_sdk.workspace = true prometheus-client.workspace = true ## Utils @@ -39,6 +38,7 @@ itertools.workspace = true schemars.workspace = true base64.workspace = true serde.workspace = true +serde_with.workspace = true serde_json.workspace = true serde_yaml.workspace = true ## k8s @@ -55,4 +55,5 @@ ssh-key.workspace = true ssh-key.features = ["ed25519"] rand_hc.workspace = true blake3.workspace = true +blake3.features = ["serde"] zeroize.workspace = true diff --git a/kuyaseg-controller/src/bin/crdgen.rs b/kuyaseg-controller/src/bin/crdgen.rs new file mode 100644 index 0000000..8045fc9 --- /dev/null +++ b/kuyaseg-controller/src/bin/crdgen.rs @@ -0,0 +1,3 @@ +use kube::CustomResourceExt; + +fn main() {} diff --git a/kuyaseg-controller/src/bin/generate_crds.rs b/kuyaseg-controller/src/bin/generate_crds.rs deleted file mode 100644 index 4390cf7..0000000 --- a/kuyaseg-controller/src/bin/generate_crds.rs +++ /dev/null @@ -1,14 +0,0 @@ -use kube::CustomResourceExt; - -fn main() { - print!( - "{}", - serde_yaml::to_string(&kuyaseg_controller::resource::generated::secret::v1alpha1::RandomSecret::crd()) - .expect("RandomSecret v1alpha1 CRD") - ); - print!( - "{}", - serde_yaml::to_string(&kuyaseg_controller::resource::generated::secret::v1alpha1::DeterministicSecret::crd()) - .expect("DeterministicSecret v1alpha1 CRD") - ) -} diff --git a/kuyaseg-controller/src/generator.rs b/kuyaseg-controller/src/generator.rs index 00baf8f..f359164 100644 --- a/kuyaseg-controller/src/generator.rs +++ b/kuyaseg-controller/src/generator.rs @@ -240,8 +240,6 @@ mod test { verify_ssh_pair(&pair); } - // ====== Deterministic (Hc128Rng) ====== - #[test] fn deterministic_bytes() { // we believe that generation is the same as for random, so our concern here is only value stability diff --git a/kuyaseg-controller/src/reporting/mod.rs b/kuyaseg-controller/src/reporting/mod.rs index 7e352a8..6132d3f 100644 --- a/kuyaseg-controller/src/reporting/mod.rs +++ b/kuyaseg-controller/src/reporting/mod.rs @@ -50,6 +50,7 @@ pub struct ReportingInit { pub pro_registry: Option, } +/// For opentelemetry TraceId to be present, tracing_opentelemetry::layer must also be set pub fn init(k8s_client: kube::Client, ct: CancellationToken) -> anyhow::Result { let config = ReportingConfig::builder() .env() diff --git a/kuyaseg-controller/src/resource/generated/secret/mod.rs b/kuyaseg-controller/src/resource/generated/secret/mod.rs index b919aba..32a5a9d 100644 --- a/kuyaseg-controller/src/resource/generated/secret/mod.rs +++ b/kuyaseg-controller/src/resource/generated/secret/mod.rs @@ -1,609 +1 @@ -use std::collections::{ - BTreeMap, - HashMap, -}; - -use anyhow::Context; -use kube::{ - Api, - Resource, -}; -use schemars::JsonSchema; -use serde::de::DeserializeOwned; -use serde::{ - Deserialize, - Serialize, -}; -use serde_json::json; -use zeroize::Zeroize; - pub mod v1alpha1; - -use crate::utils::zeroizers::{ - self, - zeroize_btree, - zeroize_hashmap, -}; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GeneratedSecretSpec { - #[serde(default)] - pub keys: Vec, - #[serde(default)] - pub template: GeneratedSecretTemplate, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GeneratedSecretStatus { - /// Stored all generated keys for matching v1/Secret resource, hashed by key name. - /// The matcher value is (version, shape hash) - /// - /// Shape hash is obtained with Blake3 hash. - pub generated: BTreeMap, -} - -impl GeneratedSecretStatus { - /// Determine whether the key from spec was already generated, based on: - /// - Key name - /// - Key version - /// - Key shape hash - fn was_generated(&self, spec: &GeneratedSecretKey) -> bool { - let Some(stored) = self.generated.get(&spec.key) else { - return false; - }; - - (stored.0 == spec.version) && (stored.1 == spec.shape.stable_hash()) - } -} - -impl GeneratedSecretSpec { - /// Reconcile generated secret from specification, following the specific type `T` generation strategy. - /// This method propagates upstream. - /// - /// This method family assumes that upstream resource has `GeneratedSecretStatus` status. - /// - /// I wrote this because - hell no - I won't write this code twice for both Random and Deterministic secrets! - async fn reconcile_spec( - &self, - name: &str, - namespace: &str, - client: kube::Client, - uid: Option<&str>, - ) -> anyhow::Result<()> - where - T: HasSecretGenerator - + Resource - + DeserializeOwned, - { - // discover target secret & branch for update/create - let target_api = Api::::namespaced(client.clone(), namespace); - let maybe_secret = scopeguard::guard( - { - target_api - .get_opt(name) - .await - .inspect_err(|e| { - tracing::error!( - debug=?e, - critical = true, - reason = "SecretDiscoveryFailed", - action = "Reconcile", - "Failed to discover target Secret" - ) - }) - .context("Discover target secret")? - }, - |mut v| { - v.as_mut().map(zeroizers::zeroize_k8s_secret); - }, - ); - - match maybe_secret.as_ref() { - Some(yep) => self - .update_from_spec::(name, namespace, client, yep) - .await - .context("Update existing Secret")?, - None => self - .create_new_from_spec::(name, namespace, client, uid) - .await - .context("Create new Secret")?, - } - - Ok(()) - } - - async fn create_new_from_spec( - &self, - name: &str, - namespace: &str, - client: kube::Client, - uid: Option<&str>, - ) -> anyhow::Result<()> - where - T: HasSecretGenerator - + Resource - + DeserializeOwned, - { - use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference; - - let self_api = Api::::namespaced(client.clone(), namespace); - let target_api = Api::::namespaced(client.clone(), namespace); - - let generated = self - .keys - .iter() - .map(|key| (key.key.clone(), (key.version, key.shape.stable_hash()))) - .collect::>(); - - // first we try with dry-run, no need to guard as no data here - let owner_ref = uid.map(|uid| OwnerReference { - api_version: T::api_version(&()).to_string(), - kind: T::kind(&()).to_string(), - name: name.to_string(), - uid: uid.to_string(), - controller: Some(true), - ..Default::default() - }); - let partial_target = { - let mut secret = self.template.to_safe_partial_secret(Some(false), Some("Opaque".into())); - secret.metadata.owner_references = owner_ref.as_ref().map(|oref| vec![oref.clone()]); - secret - }; - let patch = kube::api::Patch::Apply(json!({ - "apiVersion": T::api_version(&()), - "kind": T::kind(&()), - "status": GeneratedSecretStatus { - generated - } - })); - - // dry-run - { - target_api - .create( - &kube::api::PostParams { - dry_run: true, - field_manager: Some(env!("CARGO_PKG_NAME").into()), - }, - &partial_target, - ) - .await - .inspect_err(|e| { - tracing::error!( - debug=?e, - reason = "SecretCreationFailed", - action = "CreateNew", - "Failed to create Secret resource" - ) - }) - .context("Dry-run create Secret")?; - - self_api - .patch_status( - name, - &kube::api::PatchParams { - dry_run: true, - force: true, - field_manager: Some(env!("CARGO_PKG_NAME").into()), - field_validation: Some(kube::api::ValidationDirective::Strict), - }, - &patch, - ) - .await.inspect_err(|e| tracing::error!(debug=?e, reason="SelfUpdateFailed", action = "CreateNew", "Failed to update self status")) - .context("Dry-run update status")?; - } - - // desperate run - { - let target = scopeguard::guard( - k8s_openapi::api::core::v1::Secret { - data: Some(T::generate_data(self).context("Create new Secret")?), - ..partial_target - }, - |mut v| zeroizers::zeroize_k8s_secret(&mut v), - ); - - target_api - .create( - &kube::api::PostParams { - dry_run: false, - field_manager: Some(env!("CARGO_PKG_NAME").into()), - }, - &target, - ) - .await - .inspect_err(|e| { - tracing::error!( - debug=?e, - reason = "SecretCreationFailed", - action = "CreateNew", - "Failed to create Secret resource" - ) - }) - .context("Create Secret")?; - - if let Err(e) = self_api - .patch_status( - name, - &kube::api::PatchParams { - dry_run: false, - force: true, - field_manager: Some(env!("CARGO_PKG_NAME").into()), - field_validation: Some(kube::api::ValidationDirective::Strict), - }, - &patch, - ) - .await - .inspect_err(|e| tracing::error!(debug=?e, reason="SelfUpdateFailed", action = "CreateNew", "Failed to update self status")) - .context("Update status") - { - tracing::error!( - reason = "SecretOrphanCleanup", - action = "CreateNew", - "Deleting orphan secret after failed status update" - ); - target_api.delete(name, &kube::api::DeleteParams::default()).await.context("Orphan secret on fail")?; - return Err(e); - } - } - - Ok(()) - } - - async fn update_from_spec( - &self, - name: &str, - namespace: &str, - client: kube::Client, - old_ref: &k8s_openapi::api::core::v1::Secret, - ) -> anyhow::Result<()> - where - T: HasSecretGenerator - + Resource - + DeserializeOwned, - { - todo!("WIP") - } -} - -pub(super) trait HasSecretGenerator { - fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result>; -} - -#[cfg_attr(any(debug_assertions, test), derive(Debug, Eq, PartialEq))] -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GeneratedSecretTemplate { - /// Add custom metadata to generated secret - #[serde(default)] - pub metadata: GeneratedSecretMetadata, - /// Base64 (standard+pad) encoded data - #[serde(default)] - pub data: HashMap, - /// Verbatim string data, which will be incoded as bytes into secret - #[serde(rename = "stringData")] - #[serde(default)] - pub string_data: BTreeMap, -} - -impl GeneratedSecretTemplate { - /// Does not retain data. - pub fn to_safe_partial_secret( - &self, - immutable: Option, - type_: Option, - ) -> k8s_openapi::api::core::v1::Secret { - k8s_openapi::api::core::v1::Secret { - type_, - immutable, - data: None, - metadata: self.metadata.clone().into_partial_metadata(), - string_data: None, - } - } -} - -impl Zeroize for GeneratedSecretTemplate { - fn zeroize(&mut self) { - zeroize_btree(&mut self.string_data); - zeroize_hashmap(&mut self.data); - } -} - -impl Drop for GeneratedSecretTemplate { - fn drop(&mut self) { - self.zeroize(); - } -} - -#[cfg_attr(any(debug_assertions, test), derive(Debug, Eq, PartialEq))] -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GeneratedSecretMetadata { - /// Add annotations to generated secret - #[serde(default)] - pub annotations: BTreeMap, - /// Add labels to generated secret - #[serde(default)] - pub labels: BTreeMap, -} - -impl GeneratedSecretMetadata { - pub fn into_partial_metadata(self) -> kube::api::ObjectMeta { - kube::api::ObjectMeta { - annotations: Some(self.annotations), - labels: Some(self.labels), - ..Default::default() - } - } -} - -#[cfg_attr(any(debug_assertions, test), derive(Debug, PartialEq, Eq))] -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub struct GeneratedSecretKey { - /// The name of the key to write generated value into - pub key: String, - /// The version of generated value. Changing this will force new value to be generated - pub version: usize, - /// The type/kind specification for the generated value. May be `password` or `bytes`. Contains subfields - pub shape: GeneratedSecretShape, -} - -#[cfg_attr(any(debug_assertions, test), derive(Debug, PartialEq, Eq))] -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub enum GeneratedSecretShape { - Password { - /// The length of password in characters. Defaults to 32 - #[serde(default = "GeneratedSecretShape::default_pwd_length")] - length: usize, - /// The string of characters generator allowed to use. Defaults to alphanumeric lowercase+uppercase - #[serde(default = "GeneratedSecretShape::default_pwd_dict")] - dictionary: String, - }, - Bytes { - /// The size of secret in bytes. Defaults to 64 - #[serde(default = "GeneratedSecretShape::default_bytes_size")] - size: usize, - }, -} - -impl GeneratedSecretShape { - /// Base64 encoded stable (blake3) hash - pub fn stable_hash(&self) -> String { - use base64::prelude::*; - - let hash = { - let mut hasher = blake3::Hasher::new(); - match self { - Self::Bytes { size } => { - hasher.update(b"bytes"); - hasher.update(&size.to_le_bytes()); - } - Self::Password { length, dictionary } => { - hasher.update(b"password"); - hasher.update(&length.to_le_bytes()); - hasher.update(dictionary.as_bytes()); - } - } - hasher.finalize() - }; - - BASE64_STANDARD.encode(hash.as_bytes()) - } - - pub fn default_pwd_length() -> usize { - 32 - } - - pub fn default_pwd_dict() -> String { - String::from("abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ") - } - - pub fn default_bytes_size() -> usize { - 64 - } -} - -impl Zeroize for GeneratedSecretShape { - fn zeroize(&mut self) { - match self { - Self::Bytes { size } => { - size.zeroize(); - } - Self::Password { length, dictionary } => { - length.zeroize(); - dictionary.zeroize(); - } - } - } -} - -impl Drop for GeneratedSecretShape { - fn drop(&mut self) { - self.zeroize(); - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn deser_template_metadata() { - // Full sample - let sample = " - annotations: - my.custom-annotation: value - labels: - some-custom.label: value - "; - let expect = GeneratedSecretMetadata { - annotations: BTreeMap::from_iter([("my.custom-annotation".into(), "value".into())]), - labels: BTreeMap::from_iter([("some-custom.label".into(), "value".into())]), - }; - let parsed: GeneratedSecretMetadata = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Full sample didn't match"); - - // Empty annotations & labels - let sample = " - annotations: {} - labels: {} - "; - let expect = GeneratedSecretMetadata { - annotations: BTreeMap::new(), - labels: BTreeMap::new(), - }; - let parsed: GeneratedSecretMetadata = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Empty annotations&labels didn't match"); - - // Empty - let sample = ""; - let expect = GeneratedSecretMetadata::default(); - let parsed: GeneratedSecretMetadata = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Empty didn't match"); - } - - #[test] - fn deser_template() { - // full sample - let sample = " - data: - key0: abcde - stringData: - key1: fghij - metadata: - annotations: - my.custom-annotation: value - labels: - some-custom.label: value - "; - let expect = GeneratedSecretTemplate { - data: HashMap::from_iter([("key0".into(), "abcde".into())]), - string_data: BTreeMap::from_iter([("key1".into(), "fghij".into())]), - metadata: GeneratedSecretMetadata { - annotations: BTreeMap::from_iter([("my.custom-annotation".into(), "value".into())]), - labels: BTreeMap::from_iter([("some-custom.label".into(), "value".into())]), - }, - }; - let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Full sample didn't match"); - - // Empty metadata - let sample = " - data: - key0: abcde - stringData: - key1: fghij - metadata: {} - "; - let expect = GeneratedSecretTemplate { - data: HashMap::from_iter([("key0".into(), "abcde".into())]), - string_data: BTreeMap::from_iter([("key1".into(), "fghij".into())]), - metadata: GeneratedSecretMetadata::default(), - }; - let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Empty metadata didn't match"); - - // No metadata - let sample = " - data: - key0: abcde - stringData: - key1: fghij - "; - let expect = GeneratedSecretTemplate { - data: HashMap::from_iter([("key0".into(), "abcde".into())]), - string_data: BTreeMap::from_iter([("key1".into(), "fghij".into())]), - metadata: GeneratedSecretMetadata::default(), - }; - let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "No metadata didn't match"); - - // Empty data & stringdata - let sample = " - data: {} - stringData: {} - "; - let expect = GeneratedSecretTemplate { - data: HashMap::new(), - string_data: BTreeMap::new(), - metadata: GeneratedSecretMetadata::default(), - }; - let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Empty data&stringData didn't match"); - - // Empty - let sample = ""; - let expect = GeneratedSecretTemplate::default(); - let parsed: GeneratedSecretTemplate = serde_yaml::from_str(sample).unwrap(); - assert_eq!(parsed, expect, "Empty didn't match"); - } - - #[test] - fn deser_key_spec() { - // Full password - let sample = " - key: aabbcc - version: 12 - shape: !Password - length: 123 - dictionary: abcdefg_1 - "; - let expect = GeneratedSecretKey { - key: "aabbcc".into(), - version: 12, - shape: GeneratedSecretShape::Password { - length: 123, - dictionary: "abcdefg_1".into(), - }, - }; - let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); - assert_eq!(expect, parsed, "Full password mismatch"); - - // Empty password - let sample = " - key: aabbcc - version: 12 - shape: !Password - "; - let expect = GeneratedSecretKey { - key: "aabbcc".into(), - version: 12, - shape: GeneratedSecretShape::Password { - length: GeneratedSecretShape::default_pwd_length(), - dictionary: GeneratedSecretShape::default_pwd_dict(), - }, - }; - let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); - assert_eq!(expect, parsed, "Empty password mismatch"); - - // Full bytes - let sample = " - key: aabbcc - version: 12 - shape: !Bytes - size: 1234 - "; - let expect = GeneratedSecretKey { - key: "aabbcc".into(), - version: 12, - shape: GeneratedSecretShape::Bytes { size: 1234 }, - }; - let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); - assert_eq!(expect, parsed, "Full bytes mismatch"); - - // Empty bytes - let sample = " - key: aabbcc - version: 12 - shape: !Bytes - "; - let expect = GeneratedSecretKey { - key: "aabbcc".into(), - version: 12, - shape: GeneratedSecretShape::Bytes { - size: GeneratedSecretShape::default_bytes_size(), - }, - }; - let parsed: GeneratedSecretKey = serde_yaml::from_str(sample).unwrap(); - assert_eq!(expect, parsed, "Empty bytes mismatch"); - } -} diff --git a/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs b/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs deleted file mode 100644 index 1111bf6..0000000 --- a/kuyaseg-controller/src/resource/generated/secret/v1alpha1.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::Context; -use k8s_openapi::ByteString; -use kube::runtime::controller::Action; -use kube::{ - Client, - CustomResource, - Resource, - ResourceExt, -}; -use rand::prelude::SeedableRng; -use rand_hc::Hc128Rng; -use schemars::JsonSchema; -use serde::{ - Deserialize, - Serialize, -}; - -use crate::generator; -use crate::resource::generated::secret::{ - GeneratedSecretShape, - GeneratedSecretSpec, - GeneratedSecretStatus, - HasSecretGenerator, -}; - -/// Spec to generate random v1/Secret resource. -/// The resource is generated with the same name in the same namespace as this CR is deployed. -/// -/// The generated Secret data will change: -/// - On next reconcilation when: -/// - Generated v1/Secret was deleted (all regen) -/// - Key spec was changed (per-key regen) -/// -/// Altering the `template` *will not* change generated values -#[derive(Debug, Clone, Serialize, Deserialize, CustomResource, JsonSchema)] -#[kube( - namespaced, - kind = "RandomSecret", - group = "generator.kuyaseg.spushk.ru", - version = "v1alpha1", - status = "GeneratedSecretStatus" -)] -#[kube( - singular = "randomsecret", - plural = "randomsecrets", - shortname = "randsec", - category = "all", - category = "generators", - category = "secret-generators" -)] -#[serde(transparent)] -pub struct RandomSecretSpec(GeneratedSecretSpec); - -impl RandomSecret { - pub async fn reconcile(&self, client: Client) -> anyhow::Result { - let kind = Self::kind(&()); - let api = Self::api_version(&()); - let name = &self.name_any(); - let _span_nons = tracing::info_span!( - "RandomSecret.reconcile", - k8s_kind = kind.as_ref(), - k8s_api = api.as_ref(), - k8s_name = name, - k8s_uid = self.uid(), - ) - .entered(); - - let Some(namespace) = self.namespace() else { - tracing::error!( - reason = "RandomSecretNeedNamespace", - action = "Reconcile", - "RandomSecret must have namespace" - ); - return Err(anyhow::Error::msg("RandomSecret needed namespace")); - }; - let _span = tracing::info_span!("RandomSecret.reconcile.ns", k8s_ns = namespace).entered(); - - self.spec.0.reconcile_spec::(name, &namespace, client, self.uid().as_deref()).await.inspect_err(|e| tracing::error!(debug=?e, reason="RandomSecretReconciliationFailed", action = "Reconcile", "Failed to reconcile RandomSecret {namespace}{name}")).context("Reconcile")?; - - // TODO: verify if all desired events fire - Ok(Action::await_change()) - } -} - -impl HasSecretGenerator for RandomSecret { - fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result> { - let mut rng = generator::AwsRng::new(); - let mut data = BTreeMap::new(); - - for key in spec.keys.iter() { - match &key.shape { - GeneratedSecretShape::Bytes { size } => { - data.insert(key.key.clone(), ByteString(generator::bytes(&mut rng, *size))); - } - GeneratedSecretShape::Password { length, dictionary } => { - data.insert(key.key.clone(), ByteString(generator::str_bytes(&mut rng, *length, dictionary.chars()).inspect_err(|e| tracing::error!(debug=?e, reason="PasswordGenFailed", action="Generate", "Failed to generate new random password")).context("Generate password")?)); - } - } - } - - Ok(data) - } -} - -/// Spec to generate DETERMINISTIC v1/Secret resource. -/// The resource is generated with the same name in the same namespace as this CR is deployed. -/// -/// The generated Secret data will change on next reconcilation when key spec was changed (per-key regen) -/// -/// Altering the `template` *will not* change generated values. -/// Removing generated v1/Secret *will not* change generated values. -#[derive(Debug, Clone, Serialize, Deserialize, CustomResource, JsonSchema)] -#[kube( - namespaced, - kind = "DeterministicSecret", - group = "generator.kuyaseg.spushk.ru", - version = "v1alpha1", - status = "GeneratedSecretStatus" -)] -#[kube( - singular = "deterministicsecret", - plural = "deterministicsecrets", - shortname = "detsec", - category = "all", - category = "generators", - category = "secret-generators" -)] -#[serde(transparent)] -pub struct DeterministicSecretSpec(GeneratedSecretSpec); - -impl DeterministicSecret { - pub async fn reconcile(&self, client: Client) -> anyhow::Result { - let kind = Self::kind(&()); - let api = Self::api_version(&()); - let name = &self.name_any(); - let _span_nons = tracing::info_span!( - "DeterministicSecret.reconcile", - k8s_kind = kind.as_ref(), - k8s_api = api.as_ref(), - k8s_name = name, - k8s_uid = self.uid(), - ) - .entered(); - - let Some(namespace) = self.namespace() else { - tracing::error!( - reason = "DeterministicSecretNeedNamespace", - action = "Reconcile", - "DeterministicSecret must have namespace" - ); - return Err(anyhow::Error::msg("DeterministicSecret needed namespace")); - }; - let _span = tracing::info_span!("DeterministicSecret.reconcile.ns", k8s_ns = namespace).entered(); - - self.spec.0.reconcile_spec::(name, &namespace, client, self.uid().as_deref()).await.inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretReconciliationFailed", action = "Reconcile", "Failed to reconcile DeterministicSecret {namespace}{name}")).context("Reconcile")?; - - // TODO: verify if all desired events fire - Ok(Action::await_change()) - } -} - -impl HasSecretGenerator for DeterministicSecret { - fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result> { - let seed = std::env::var("KUYASEG_SEED").inspect_err(|e| tracing::error!(debug=?e, reason="DeterministicSecretNeedSeed", "Refuse to generate deterministic secret without seed. Set with KUYASEG_SEED env")).context("Get external seed")?; - let mut data = BTreeMap::new(); - - for key in spec.keys.iter() { - let mut rng = { - let mut hasher = blake3::Hasher::new(); - hasher.update(seed.as_bytes()); - hasher.update(key.key.as_bytes()); - hasher.update(&key.version.to_le_bytes()); - hasher.update(key.shape.stable_hash().as_bytes()); - Hc128Rng::from_seed(hasher.finalize().into()) - }; - - match &key.shape { - GeneratedSecretShape::Bytes { size } => { - data.insert(key.key.clone(), ByteString(generator::bytes(&mut rng, *size))); - } - GeneratedSecretShape::Password { length, dictionary } => { - data.insert(key.key.clone(), ByteString(generator::str_bytes(&mut rng, *length, dictionary.chars()).inspect_err(|e| tracing::error!(debug=?e, reason="PasswordGenFailed", action="Generate", "Failed to generate new random password")).context("Generate password")?)); - } - } - } - - Ok(data) - } -} diff --git a/kuyaseg-controller/src/resource/generated/secret/v1alpha1/mod.rs b/kuyaseg-controller/src/resource/generated/secret/v1alpha1/mod.rs new file mode 100644 index 0000000..871bd5b --- /dev/null +++ b/kuyaseg-controller/src/resource/generated/secret/v1alpha1/mod.rs @@ -0,0 +1,354 @@ +use std::collections::{ + BTreeMap, + HashMap, +}; + +use k8s_openapi::ByteString; +use schemars::JsonSchema; +use serde::{ + Deserialize, + Serialize, +}; +use serde_with::serde_as; +use zeroize::Zeroize; + +use crate::utils::{ + serializers, + zeroizers, +}; + +mod pseudorandom; +pub use pseudorandom::*; + +mod random; +pub use random::*; + +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct SecretStatus { + pub key_tracker: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde_as] +pub struct KeyStatus { + /// Version is tracked separately, for change must set NE status + pub version: usize, + #[schemars(schema_with = "String::json_schema")] + #[serde_as(as = "serde_with::hex::Hex")] + pub spec_hash: blake3::Hash, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] +pub struct KeySpec { + /// Name of the key to put generated data into + pub key: String, + /// Tracks the key generation version within the same shape. + /// Increasing this number will force value regeneration on next reconcilation + pub version: usize, + /// Determines the shape of generated data + pub shape: KeyShape, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] +pub enum KeyShape { + /// Generate arbitrary bytes + Bytes { + /// Size of generated sequence in bytes. + /// + /// Defaults to 64 + #[serde(default = "KeyShape::default_bytes_size")] + size: usize, + }, + /// Generate string password + Password { + /// The length of password in characters. + /// + /// Defaults to 24 + #[serde(default = "KeyShape::default_pwd_length")] + length: usize, + /// The string of characters allowed to be present in password. + /// When dictionary length is > 4, each character in generated password is guaranteed to repeat + /// with period at least 3. + /// + /// Defaults to lowercase+uppercase alphanumeric and `-` + #[serde(default = "KeyShape::default_pwd_dict")] + dictionary: String, + }, +} + +impl KeyShape { + /// BLAKE3 hash, incorporating shape and according properties + pub fn stable_hash(&self) -> blake3::Hash { + let mut hasher = blake3::Hasher::new(); + match self { + Self::Bytes { size } => { + hasher.update(b"bytes"); + hasher.update(&size.to_le_bytes()); + } + Self::Password { length, dictionary } => { + hasher.update(b"password"); + hasher.update(&length.to_le_bytes()); + hasher.update(dictionary.as_bytes()); + } + } + hasher.finalize() + } + + pub fn default_bytes_size() -> usize { + 64 + } + pub fn default_pwd_length() -> usize { + 24 + } + pub fn default_pwd_dict() -> String { + String::from("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-") + } +} + +impl Zeroize for KeyShape { + #[inline] + fn zeroize(&mut self) { + match self { + Self::Bytes { size } => size.zeroize(), + Self::Password { length, dictionary } => { + length.zeroize(); + dictionary.zeroize(); + } + } + } +} + +impl Drop for KeyShape { + fn drop(&mut self) { + self.zeroize(); + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SecretTemplate { + /// Define created Secret resource metadata + #[serde(default)] + pub metadata: SecretMetadata, + /// Include static data into created Secret + #[serde(default)] + #[schemars(schema_with = "BTreeMap::::json_schema")] + pub data: BTreeMap, + /// Include static string data into created Secret + #[serde(default)] + #[serde(rename = "stringData")] + #[serde(with = "serializers::btreemap_to_bytestring_inplace")] + #[schemars(schema_with = "BTreeMap::::json_schema")] + pub string_data: BTreeMap, +} + +impl Zeroize for SecretTemplate { + #[inline] + fn zeroize(&mut self) { + zeroizers::zeroize_btree_of_bytestring(&mut self.data); + zeroizers::zeroize_btree_of_bytestring(&mut self.string_data); + } +} + +impl Drop for SecretTemplate { + fn drop(&mut self) { + self.zeroize(); + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SecretMetadata { + #[serde(default)] + pub annotations: BTreeMap, + #[serde(default)] + pub labels: BTreeMap, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn deser_template_metadata() { + // Full sample + let sample = " + annotations: + my.custom-annotation: value + labels: + some-custom.label: value + "; + let expect = SecretMetadata { + annotations: BTreeMap::from_iter([("my.custom-annotation".into(), "value".into())]), + labels: BTreeMap::from_iter([("some-custom.label".into(), "value".into())]), + }; + let parsed: SecretMetadata = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Full sample didn't match"); + + // Empty annotations & labels + let sample = " + annotations: {} + labels: {} + "; + let expect = SecretMetadata { + annotations: BTreeMap::new(), + labels: BTreeMap::new(), + }; + let parsed: SecretMetadata = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty annotations&labels didn't match"); + + // Empty + let sample = ""; + let expect = SecretMetadata::default(); + let parsed: SecretMetadata = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty didn't match"); + } + + #[test] + fn deser_template() { + // full sample + let sample = " + data: + key0: YWJjZGU= + stringData: + key1: fghij + metadata: + annotations: + my.custom-annotation: value + labels: + some-custom.label: value + "; + let expect = SecretTemplate { + data: BTreeMap::from_iter([("key0".into(), ByteString("abcde".to_string().into_bytes()))]), + string_data: BTreeMap::from_iter([("key1".into(), ByteString("fghij".to_string().into_bytes()))]), + metadata: SecretMetadata { + annotations: BTreeMap::from_iter([("my.custom-annotation".into(), "value".into())]), + labels: BTreeMap::from_iter([("some-custom.label".into(), "value".into())]), + }, + }; + let parsed: SecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Full sample didn't match"); + + // Empty metadata + let sample = " + data: + key0: YWJjZGU= + stringData: + key1: fghij + metadata: {} + "; + let expect = SecretTemplate { + data: BTreeMap::from_iter([("key0".into(), ByteString("abcde".to_string().into_bytes()))]), + string_data: BTreeMap::from_iter([("key1".into(), ByteString("fghij".to_string().into_bytes()))]), + metadata: SecretMetadata::default(), + }; + let parsed: SecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty metadata didn't match"); + + // No metadata + let sample = " + data: + key0: YWJjZGU= + stringData: + key1: fghij + "; + let expect = SecretTemplate { + data: BTreeMap::from_iter([("key0".into(), ByteString("abcde".to_string().into_bytes()))]), + string_data: BTreeMap::from_iter([("key1".into(), ByteString("fghij".to_string().into_bytes()))]), + metadata: SecretMetadata::default(), + }; + let parsed: SecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "No metadata didn't match"); + + // Empty data & stringdata + let sample = " + data: {} + stringData: {} + "; + let expect = SecretTemplate { + data: BTreeMap::new(), + string_data: BTreeMap::new(), + metadata: SecretMetadata::default(), + }; + let parsed: SecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty data&stringData didn't match"); + + // Empty + let sample = ""; + let expect = SecretTemplate::default(); + let parsed: SecretTemplate = serde_yaml::from_str(sample).unwrap(); + assert_eq!(parsed, expect, "Empty didn't match"); + } + + #[test] + fn deser_key_spec() { + // Full password + let sample = " + key: aabbcc + version: 12 + shape: !Password + length: 123 + dictionary: abcdefg_1 + "; + let expect = KeySpec { + key: "aabbcc".into(), + version: 12, + shape: KeyShape::Password { + length: 123, + dictionary: "abcdefg_1".into(), + }, + }; + let parsed: KeySpec = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Full password mismatch"); + + // Empty password + let sample = " + key: aabbcc + version: 12 + shape: !Password + "; + let expect = KeySpec { + key: "aabbcc".into(), + version: 12, + shape: KeyShape::Password { + length: KeyShape::default_pwd_length(), + dictionary: KeyShape::default_pwd_dict(), + }, + }; + let parsed: KeySpec = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Empty password mismatch"); + + // Full bytes + let sample = " + key: aabbcc + version: 12 + shape: !Bytes + size: 1234 + "; + let expect = KeySpec { + key: "aabbcc".into(), + version: 12, + shape: KeyShape::Bytes { size: 1234 }, + }; + let parsed: KeySpec = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Full bytes mismatch"); + + // Empty bytes + let sample = " + key: aabbcc + version: 12 + shape: !Bytes + "; + let expect = KeySpec { + key: "aabbcc".into(), + version: 12, + shape: KeyShape::Bytes { + size: KeyShape::default_bytes_size(), + }, + }; + let parsed: KeySpec = serde_yaml::from_str(sample).unwrap(); + assert_eq!(expect, parsed, "Empty bytes mismatch"); + } +} diff --git a/kuyaseg-controller/src/resource/generated/secret/v1alpha1/pseudorandom.rs b/kuyaseg-controller/src/resource/generated/secret/v1alpha1/pseudorandom.rs new file mode 100644 index 0000000..e69de29 diff --git a/kuyaseg-controller/src/resource/generated/secret/v1alpha1/random.rs b/kuyaseg-controller/src/resource/generated/secret/v1alpha1/random.rs new file mode 100644 index 0000000..e69de29 diff --git a/kuyaseg-controller/src/resource/generated/ssh/mod.rs b/kuyaseg-controller/src/resource/generated/ssh/mod.rs index e51933e..8b13789 100644 --- a/kuyaseg-controller/src/resource/generated/ssh/mod.rs +++ b/kuyaseg-controller/src/resource/generated/ssh/mod.rs @@ -1 +1 @@ -pub type GeneratedSshSecretTemplate = super::secret::GeneratedSecretTemplate; + diff --git a/kuyaseg-controller/src/utils/mod.rs b/kuyaseg-controller/src/utils/mod.rs index 0269ccf..659e158 100644 --- a/kuyaseg-controller/src/utils/mod.rs +++ b/kuyaseg-controller/src/utils/mod.rs @@ -1 +1,2 @@ +pub mod serializers; pub mod zeroizers; diff --git a/kuyaseg-controller/src/utils/serializers.rs b/kuyaseg-controller/src/utils/serializers.rs new file mode 100644 index 0000000..83468f5 --- /dev/null +++ b/kuyaseg-controller/src/utils/serializers.rs @@ -0,0 +1,106 @@ +/// in-place conversion of String into ByteString, verbatim, utf8-lossy +/// +/// e.g. `"abc" -> ByteString("abc".to_string().into_bytes())` +pub mod str_to_bytestring_inplace { + use k8s_openapi::ByteString; + use serde::de::Visitor; + use serde::{ + Deserializer, + Serializer, + }; + + pub fn serialize(value: &ByteString, se: S) -> Result + where + S: Serializer, + { + let value = String::from_utf8_lossy(&value.0); + se.serialize_str(&value) + } + + pub fn deserialize<'de, D>(de: D) -> Result + where + D: Deserializer<'de>, + { + de.deserialize_str(StringBytestringVisitor) + } + + struct StringBytestringVisitor; + + impl<'de> Visitor<'de> for StringBytestringVisitor { + type Value = ByteString; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(ByteString(v.as_bytes().into())) + } + } +} + +/// in-place conversion of BTreeMap to BTreeMap, verbatim, utf8-lossy +/// +///e.g. `{"abc": "def"} -> {"abc": ByteString("def".to_string().into_bytes())}` +pub mod btreemap_to_bytestring_inplace { + use std::collections::BTreeMap; + use std::marker::PhantomData; + + use k8s_openapi::ByteString; + use serde::de::Visitor; + use serde::ser::SerializeMap; + use serde::{ + Deserialize, + Deserializer, + Serialize, + Serializer, + }; + + pub fn serialize(value: &BTreeMap, se: S) -> Result + where + S: Serializer, + K: Serialize, + { + let mut map = se.serialize_map(Some(value.len()))?; + value + .iter() + .try_for_each(|(k, v)| map.serialize_entry(k, &String::from_utf8_lossy(&v.0)))?; + map.end() + } + + pub fn deserialize<'de, K, D>(de: D) -> Result, D::Error> + where + D: Deserializer<'de>, + K: Ord + Deserialize<'de>, + { + de.deserialize_map(BTreeVisitor(PhantomData)) + } + + struct BTreeVisitor(PhantomData); + + impl<'de, K: Ord + Deserialize<'de>> Visitor<'de> for BTreeVisitor { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "map {ty} -> String", ty = std::any::type_name::()) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut btree = BTreeMap::new(); + while let Some((key, value)) = map.next_entry::()? { + if btree.contains_key(&key) { + return Err(serde::de::Error::custom("Duplicate key")); + } + btree.insert(key, ByteString(value.as_bytes().into())); + } + + Ok(btree) + } + } +} From e7748880e000a0ec7aff239f7cca4b46689fd6f7 Mon Sep 17 00:00:00 2001 From: spushk Date: Wed, 21 Oct 2026 03:01:54 +0300 Subject: [PATCH 10/10] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aaef0ac..ee95cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .opencode +.envrc