chore!: minor fixes here and there & move CRDs into submods & introduce serializer helpers
This commit is contained in:
parent
e7f6ddf741
commit
3ac37809b7
13 changed files with 811 additions and 1543 deletions
|
|
@ -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
|
||||
|
|
|
|||
3
kuyaseg-controller/src/bin/crdgen.rs
Normal file
3
kuyaseg-controller/src/bin/crdgen.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
use kube::CustomResourceExt;
|
||||
|
||||
fn main() {}
|
||||
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ pub struct ReportingInit {
|
|||
pub pro_registry: Option<prometheus_client::registry::Registry>,
|
||||
}
|
||||
|
||||
/// For opentelemetry TraceId to be present, tracing_opentelemetry::layer must also be set
|
||||
pub fn init(k8s_client: kube::Client, ct: CancellationToken) -> anyhow::Result<ReportingInit> {
|
||||
let config = ReportingConfig::builder()
|
||||
.env()
|
||||
|
|
|
|||
|
|
@ -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<GeneratedSecretKey>,
|
||||
#[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<String, (usize, String)>,
|
||||
}
|
||||
|
||||
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<T>(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: &str,
|
||||
client: kube::Client,
|
||||
uid: Option<&str>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
T: HasSecretGenerator
|
||||
+ Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
|
||||
+ DeserializeOwned,
|
||||
{
|
||||
// discover target secret & branch for update/create
|
||||
let target_api = Api::<k8s_openapi::api::core::v1::Secret>::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::<T>(name, namespace, client, yep)
|
||||
.await
|
||||
.context("Update existing Secret")?,
|
||||
None => self
|
||||
.create_new_from_spec::<T>(name, namespace, client, uid)
|
||||
.await
|
||||
.context("Create new Secret")?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_new_from_spec<T>(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: &str,
|
||||
client: kube::Client,
|
||||
uid: Option<&str>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
T: HasSecretGenerator
|
||||
+ Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
|
||||
+ DeserializeOwned,
|
||||
{
|
||||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
|
||||
|
||||
let self_api = Api::<T>::namespaced(client.clone(), namespace);
|
||||
let target_api = Api::<k8s_openapi::api::core::v1::Secret>::namespaced(client.clone(), namespace);
|
||||
|
||||
let generated = self
|
||||
.keys
|
||||
.iter()
|
||||
.map(|key| (key.key.clone(), (key.version, key.shape.stable_hash())))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// 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<T>(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: &str,
|
||||
client: kube::Client,
|
||||
old_ref: &k8s_openapi::api::core::v1::Secret,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
T: HasSecretGenerator
|
||||
+ Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
|
||||
+ DeserializeOwned,
|
||||
{
|
||||
todo!("WIP")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait HasSecretGenerator {
|
||||
fn generate_data(spec: &GeneratedSecretSpec) -> anyhow::Result<BTreeMap<String, k8s_openapi::ByteString>>;
|
||||
}
|
||||
|
||||
#[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<String, String>,
|
||||
/// Verbatim string data, which will be incoded as bytes into secret
|
||||
#[serde(rename = "stringData")]
|
||||
#[serde(default)]
|
||||
pub string_data: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl GeneratedSecretTemplate {
|
||||
/// Does not retain data.
|
||||
pub fn to_safe_partial_secret(
|
||||
&self,
|
||||
immutable: Option<bool>,
|
||||
type_: Option<String>,
|
||||
) -> 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<String, String>,
|
||||
/// Add labels to generated secret
|
||||
#[serde(default)]
|
||||
pub labels: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Action> {
|
||||
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::<Self>(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<BTreeMap<String, ByteString>> {
|
||||
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<Action> {
|
||||
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::<Self>(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<BTreeMap<String, ByteString>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
354
kuyaseg-controller/src/resource/generated/secret/v1alpha1/mod.rs
Normal file
354
kuyaseg-controller/src/resource/generated/secret/v1alpha1/mod.rs
Normal file
|
|
@ -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<String, KeyStatus>,
|
||||
}
|
||||
|
||||
#[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<serde_with::formats::Uppercase>")]
|
||||
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::<String, String>::json_schema")]
|
||||
pub data: BTreeMap<String, ByteString>,
|
||||
/// Include static string data into created Secret
|
||||
#[serde(default)]
|
||||
#[serde(rename = "stringData")]
|
||||
#[serde(with = "serializers::btreemap_to_bytestring_inplace")]
|
||||
#[schemars(schema_with = "BTreeMap::<String, String>::json_schema")]
|
||||
pub string_data: BTreeMap<String, ByteString>,
|
||||
}
|
||||
|
||||
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<String, String>,
|
||||
#[serde(default)]
|
||||
pub labels: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
pub type GeneratedSshSecretTemplate = super::secret::GeneratedSecretTemplate;
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
pub mod serializers;
|
||||
pub mod zeroizers;
|
||||
|
|
|
|||
106
kuyaseg-controller/src/utils/serializers.rs
Normal file
106
kuyaseg-controller/src/utils/serializers.rs
Normal file
|
|
@ -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<S>(value: &ByteString, se: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let value = String::from_utf8_lossy(&value.0);
|
||||
se.serialize_str(&value)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(de: D) -> Result<ByteString, D::Error>
|
||||
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<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(ByteString(v.as_bytes().into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// in-place conversion of BTreeMap<K, String> to BTreeMap<K, ByteString>, 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<S, K>(value: &BTreeMap<K, ByteString>, se: S) -> Result<S::Ok, S::Error>
|
||||
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<BTreeMap<K, ByteString>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
K: Ord + Deserialize<'de>,
|
||||
{
|
||||
de.deserialize_map(BTreeVisitor(PhantomData))
|
||||
}
|
||||
|
||||
struct BTreeVisitor<K>(PhantomData<K>);
|
||||
|
||||
impl<'de, K: Ord + Deserialize<'de>> Visitor<'de> for BTreeVisitor<K> {
|
||||
type Value = BTreeMap<K, ByteString>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "map {ty} -> String", ty = std::any::type_name::<K>())
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut btree = BTreeMap::new();
|
||||
while let Some((key, value)) = map.next_entry::<K, &str>()? {
|
||||
if btree.contains_key(&key) {
|
||||
return Err(serde::de::Error::custom("Duplicate key"));
|
||||
}
|
||||
btree.insert(key, ByteString(value.as_bytes().into()));
|
||||
}
|
||||
|
||||
Ok(btree)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue