Compare commits

..
24 changed files with 1050 additions and 1112 deletions

3
.gitignore vendored
View file

@ -1,3 +0,0 @@
/target
.opencode
.envrc

1031
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,16 +18,18 @@ mimalloc.version = "^0.1"
mimalloc.features = ["secure"] mimalloc.features = ["secure"]
tokio.version = "^1" tokio.version = "^1"
tokio.features = ["macros", "rt-multi-thread"] tokio.features = ["macros", "rt-multi-thread"]
tokio-util.version = '^0.7'
axum.version = "^0.8" axum.version = "^0.8"
## Tracing ## Tracing
tracing.version = "^0.1" tracing.version = "^0.1"
tracing.features = ["max_level_debug", "release_max_level_info"] tracing.features = ["max_level_trace", "release_max_level_info"]
tracing-subscriber.version = "^0.3" tracing-subscriber.version = "^0.3"
tracing-opentelemetry.version = "^0.33" tracing-opentelemetry.version = "^0.32"
opentelemetry.version = "^0.32" opentelemetry.version = "^0.31"
opentelemetry_sdk.version = "^0.32" opentelemetry.features = ["trace"]
prometheus-client.version = "^0.24" opentelemetry-otlp.version = "^0.31"
opentelemetry-otlp.features = ["grpc-tonic", "reqwest-rustls"]
opentelemetry_sdk.version = "^0.31"
opentelemetry_sdk.features = ["rt-tokio"]
## Utils ## Utils
jiff.version = "^0.2" jiff.version = "^0.2"
scopeguard.version = "^1" scopeguard.version = "^1"
@ -39,8 +41,6 @@ schemars.version = "^1"
base64.version = "^0.22" base64.version = "^0.22"
serde.version = "^1" serde.version = "^1"
serde.features = ["derive"] serde.features = ["derive"]
serde_with.version = "3"
serde_with.features = ["hex", "schemars_1"]
serde_json.version = "^1" serde_json.version = "^1"
serde_yaml.version = "^0.9" serde_yaml.version = "^0.9"
## k8s ## k8s
@ -48,6 +48,7 @@ kube.version = "^3"
kube.features = ["derive"] kube.features = ["derive"]
k8s-openapi.version = "^0.27" k8s-openapi.version = "^0.27"
k8s-openapi.features = ["latest", "schemars"] k8s-openapi.features = ["latest", "schemars"]
prometheus-client.version = "^0.24"
## Codegen ## Codegen
thiserror.version = "^2" thiserror.version = "^2"
anyhow.version = "^1" anyhow.version = "^1"

185
README.md
View file

@ -1,185 +0,0 @@
# 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
### 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
- key: binary-key
# Change to regenerate value without altering spec
version: 0
# Determine secret shape
shape: !Bytes
# Set size of value in bytes
size: 64
# Password key - guaranteed UTF-8 character sequence with repeat period >= 3
- key: password-key
version: 0
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: {}
```
#### 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:
- `<name>-identity`: private SSH key, which is stored in specified key
- `<name>-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 (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.
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
shape: !Password
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
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.
Here is a comparison.
||Kuyaseg|kubernetes-secret-generator|ESO||
|-|-|-|-|-|
|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||
|Target secret template|Respects <ul><li>annotations</li> <li>labels</li></ul>|[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
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` 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.

View file

@ -11,14 +11,13 @@ publish.workspace = true
[[bin]] [[bin]]
doc = false doc = false
name = "crdgen" name = "generate_crds"
path = "src/bin/crdgen.rs" path = "src/bin/generate_crds.rs"
[dependencies] [dependencies]
## Rt ## Rt
mimalloc.workspace = true mimalloc.workspace = true
tokio.workspace = true tokio.workspace = true
tokio-util.workspace = true
axum.workspace = true axum.workspace = true
## Tracing ## Tracing
tracing.workspace = true tracing.workspace = true
@ -26,6 +25,7 @@ tracing-subscriber.workspace = true
tracing-subscriber.features = ["env-filter"] tracing-subscriber.features = ["env-filter"]
tracing-opentelemetry.workspace = true tracing-opentelemetry.workspace = true
opentelemetry.workspace = true opentelemetry.workspace = true
opentelemetry-otlp.workspace = true
opentelemetry_sdk.workspace = true opentelemetry_sdk.workspace = true
prometheus-client.workspace = true prometheus-client.workspace = true
## Utils ## Utils
@ -38,7 +38,6 @@ itertools.workspace = true
schemars.workspace = true schemars.workspace = true
base64.workspace = true base64.workspace = true
serde.workspace = true serde.workspace = true
serde_with.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
## k8s ## k8s
@ -55,5 +54,4 @@ ssh-key.workspace = true
ssh-key.features = ["ed25519"] ssh-key.features = ["ed25519"]
rand_hc.workspace = true rand_hc.workspace = true
blake3.workspace = true blake3.workspace = true
blake3.features = ["serde"]
zeroize.workspace = true zeroize.workspace = true

View file

@ -1,3 +0,0 @@
use kube::CustomResourceExt;
fn main() {}

View file

@ -0,0 +1 @@
fn main() {}

View file

@ -30,15 +30,10 @@ pub fn bytes<G: CryptoRng>(rng: &mut G, size: usize) -> Vec<u8> {
buf buf
} }
pub fn str_bytes<G: CryptoRng>( pub fn str_bytes<G: CryptoRng>(rng: &mut G, length: usize, dictionary: &[char]) -> Result<Vec<u8>, GeneratorErr> {
rng: &mut G,
length: usize,
dictionary: impl Iterator<Item = char>,
) -> Result<Vec<u8>, GeneratorErr> {
let mut buf = String::with_capacity(length); let mut buf = String::with_capacity(length);
// TODO: unique may leak dictionary..? let dictionary = Zeroizing::new(dictionary.iter().unique().cloned().collect::<Vec<char>>());
let dictionary = Zeroizing::new(dictionary.unique().collect::<Vec<char>>());
let distribution = rand::distr::slice::Choose::new(&dictionary).map_err(|_| GeneratorErr::EmptyDistribution)?; let distribution = rand::distr::slice::Choose::new(&dictionary).map_err(|_| GeneratorErr::EmptyDistribution)?;
let dict_size = dictionary.len(); let dict_size = dictionary.len();
@ -70,13 +65,13 @@ pub fn str_bytes<G: CryptoRng>(
} }
if buf.len() != length { if buf.len() != length {
return Err(GeneratorErr::RngStuck { Err(GeneratorErr::RngStuck {
reason: format!("Unable to sample {length} characters"), reason: format!("Unable to sample {length} characters"),
}); })
} } else {
Ok(buf.into_bytes()) Ok(buf.into_bytes())
} }
}
/// Generate SSH keypair, encoded for OpenSSH /// Generate SSH keypair, encoded for OpenSSH
pub fn ssh_pair<G: CryptoRng>(rng: &mut G, algorithm: SshAlgorithm) -> Result<SshPair, GeneratorErr> { pub fn ssh_pair<G: CryptoRng>(rng: &mut G, algorithm: SshAlgorithm) -> Result<SshPair, GeneratorErr> {
@ -199,11 +194,11 @@ mod test {
let mut rng = AwsRng::new(); let mut rng = AwsRng::new();
let dict = dict_alnum(32); let dict = dict_alnum(32);
let gen0 = str_bytes(&mut rng, 0, dict.iter().cloned()).unwrap(); let gen0 = str_bytes(&mut rng, 0, &dict).unwrap();
let gen1 = str_bytes(&mut rng, 1, dict.iter().cloned()).unwrap(); let gen1 = str_bytes(&mut rng, 1, &dict).unwrap();
let gen27 = str_bytes(&mut rng, 27, dict.iter().cloned()).unwrap(); let gen27 = str_bytes(&mut rng, 27, &dict).unwrap();
let gen256 = str_bytes(&mut rng, 256, dict.iter().cloned()).unwrap(); let gen256 = str_bytes(&mut rng, 256, &dict).unwrap();
assert_ne!(str_bytes(&mut rng, 256, dict.iter().cloned()).unwrap(), gen256); assert_ne!(str_bytes(&mut rng, 256, &dict).unwrap(), gen256);
// length // length
assert_eq!(gen0.len(), 0); assert_eq!(gen0.len(), 0);
@ -227,7 +222,7 @@ mod test {
let mut rng = AwsRng::new(); let mut rng = AwsRng::new();
let dict: Vec<char> = dict_alnum(3); let dict: Vec<char> = dict_alnum(3);
let gen16 = str_bytes(&mut rng, 16, dict.iter().cloned()).unwrap(); let gen16 = str_bytes(&mut rng, 16, &dict).unwrap();
assert_eq!(gen16.len(), 16); assert_eq!(gen16.len(), 16);
assert_dict_chars(&gen16, &dict); assert_dict_chars(&gen16, &dict);
@ -240,6 +235,8 @@ mod test {
verify_ssh_pair(&pair); verify_ssh_pair(&pair);
} }
// ====== Deterministic (Hc128Rng) ======
#[test] #[test]
fn deterministic_bytes() { fn deterministic_bytes() {
// we believe that generation is the same as for random, so our concern here is only value stability // we believe that generation is the same as for random, so our concern here is only value stability
@ -261,12 +258,12 @@ mod test {
let dict = dict_alnum(32); let dict = dict_alnum(32);
assert_eq!( assert_eq!(
str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, dict.iter().cloned()).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).unwrap()
); );
assert_ne!( assert_ne!(
str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, dict.iter().cloned()).unwrap(), str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, &dict).unwrap(),
str_bytes(&mut Hc128Rng::seed_from_u64(43), 256, dict.iter().cloned()).unwrap() str_bytes(&mut Hc128Rng::seed_from_u64(43), 256, &dict).unwrap()
); );
} }

View file

@ -1,4 +1,5 @@
#![feature(sync_nonpoison, nonpoison_rwlock)]
pub mod generator; pub mod generator;
pub mod reporting; pub mod reporting;
pub mod resource;
pub mod utils; pub mod utils;

View file

@ -0,0 +1,36 @@
use std::fmt::Debug;
use k8s_openapi::api::core::v1::ObjectReference;
use kube::runtime::events::{
Event,
Recorder,
};
pub struct K8sEvent {
pub object: ObjectReference,
pub event: Event,
}
pub struct K8sEventRecorder {
recorder: Recorder,
}
impl K8sEventRecorder {
pub fn new(recorder: Recorder) -> Self {
Self { recorder }
}
pub async fn record(&self, event: &K8sEvent) {
_ = self
.recorder
.publish(&event.event, &event.object)
.await
.inspect_err(|e| tracing::error!(no_event=true, error=?e, "Failed to record K8s event"));
}
}
impl Debug for K8sEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("K8sEvent").finish()
}
}

View file

@ -4,7 +4,6 @@ use std::time::Duration;
use anyhow::Context; use anyhow::Context;
use confique::Config; use confique::Config;
use opentelemetry::TraceId;
use tokio::sync::broadcast::{ use tokio::sync::broadcast::{
self, self,
Sender, Sender,
@ -12,27 +11,19 @@ use tokio::sync::broadcast::{
use tokio::task::JoinSet; use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::reporting::k8s::K8sEvent; use crate::reporting::k8s_event::K8sEvent;
use crate::reporting::otel::OtelObservation;
use crate::reporting::prometheus::ProLabel; use crate::reporting::prometheus::ProLabel;
use crate::reporting::tracing_evt::{ use crate::reporting::tracing_evt::{
TracingK8sEventLayer, TracingK8sEventLayer,
TracingK8sEventLayerConfig, TracingK8sEventLayerConfig,
}; };
pub mod k8s; pub mod k8s_event;
pub mod otel;
pub mod prometheus; pub mod prometheus;
pub mod tracing_evt; pub mod tracing_evt;
pub fn span_trace_id() -> Option<TraceId> {
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)] #[derive(Config)]
pub struct ReportingConfig { pub struct ReportingConfig {
#[config(env = "KUYASEG_REPORT_K8S")] #[config(env = "KUYASEG_REPORT_K8S")]
@ -41,16 +32,24 @@ pub struct ReportingConfig {
#[config(env = "KUYASEG_REPORT_PROMETHEUS")] #[config(env = "KUYASEG_REPORT_PROMETHEUS")]
#[config(default = false)] #[config(default = false)]
pub prometheus: bool, pub prometheus: bool,
#[config(env = "KUYASEG_REPORT_OTEL")]
#[config(default = false)]
pub opentelemetry: bool,
}
pub fn trace_id_str(trace_id: &opentelemetry::TraceId) -> Option<String> {
match trace_id {
&opentelemetry::TraceId::INVALID => None,
x => Some(x.to_string()),
}
} }
/// You should await on handle somewhere /// You should await on handle somewhere
pub struct ReportingInit { pub struct ReportingInit {
pub tracing_layer: TracingK8sEventLayer, pub tracing_layer: TracingK8sEventLayer,
pub handle: JoinSet<()>, pub handle: JoinSet<()>,
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> { pub fn init(k8s_client: kube::Client, ct: CancellationToken) -> anyhow::Result<ReportingInit> {
let config = ReportingConfig::builder() let config = ReportingConfig::builder()
.env() .env()
@ -62,18 +61,21 @@ pub fn init(k8s_client: kube::Client, ct: CancellationToken) -> anyhow::Result<R
let k8s_tx = config let k8s_tx = config
.k8s_events .k8s_events
.then(|| init_k8s_events(k8s_client, &mut reporting_jobs, ct.clone())); .then(|| init_k8s_events(k8s_client, &mut reporting_jobs, ct.clone()));
let (pro_tx, pro_registry) = config let pro_tx = config
.prometheus .prometheus
.then(|| init_pro_metrics(&mut reporting_jobs, ct.clone())) .then(|| init_pro_metrics(&mut reporting_jobs, ct.clone()));
.unzip(); let otel_tx = config
.opentelemetry
.then(|| init_otel(&mut reporting_jobs, ct.clone()))
.transpose()?;
Ok(ReportingInit { Ok(ReportingInit {
tracing_layer: TracingK8sEventLayer::new(TracingK8sEventLayerConfig { tracing_layer: TracingK8sEventLayer::new(TracingK8sEventLayerConfig {
k8s_events: k8s_tx, k8s_events: k8s_tx,
prometheus: pro_tx, prometheus: pro_tx,
otel: otel_tx,
}), }),
handle: reporting_jobs, handle: reporting_jobs,
pro_registry,
}) })
} }
@ -83,7 +85,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
Reporter, Reporter,
}; };
use crate::reporting::k8s::K8sEventRecorder; use crate::reporting::k8s_event::K8sEventRecorder;
const WARN_THRESHOLD: usize = 10; const WARN_THRESHOLD: usize = 10;
const MAX_CAPACITY: usize = 100; const MAX_CAPACITY: usize = 100;
@ -124,7 +126,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
} }
queue.push_back(ev); queue.push_back(ev);
if queue.len() > WARN_THRESHOLD && !warned { if queue.len() > WARN_THRESHOLD && !warned {
tracing::warn!(k13t.no_report=true, debug=?queue.len(), "K8s event queue exceeded threshold"); tracing::warn!(no_event=true, queue_len=queue.len(), "K8s event queue exceeded threshold");
warned = true; warned = true;
} else if queue.len() <= WARN_THRESHOLD { } else if queue.len() <= WARN_THRESHOLD {
warned = false; warned = false;
@ -133,7 +135,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
rec_limit = 4; rec_limit = 4;
} }
Err(e) => { Err(e) => {
tracing::error!(k13t.no_report=true, debug=?e, "Failed to receive k8s event from tracing"); tracing::error!(no_event=true, debug=?e, "Failed to receive k8s event from tracing");
rx = rtx.subscribe(); // dumb recover strategy rx = rtx.subscribe(); // dumb recover strategy
tokio::time::sleep(Duration::from_secs(3)).await; tokio::time::sleep(Duration::from_secs(3)).await;
rec_limit -= 1; rec_limit -= 1;
@ -143,7 +145,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
} }
} }
tracing::error!( tracing::error!(
k13t.no_report = true, no_event = true,
"K8s Event reporting from tracing stopped" "K8s Event reporting from tracing stopped"
); );
}); });
@ -151,10 +153,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
tx tx
} }
fn init_pro_metrics( fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender<Arc<ProLabel>> {
joinset: &mut JoinSet<()>,
ct: CancellationToken,
) -> (Sender<Arc<ProLabel>>, prometheus_client::registry::Registry) {
use prometheus_client::registry::Registry; use prometheus_client::registry::Registry;
use crate::reporting::prometheus::ProMetrics; use crate::reporting::prometheus::ProMetrics;
@ -162,7 +161,10 @@ fn init_pro_metrics(
let (tx, mut rx) = broadcast::channel::<Arc<ProLabel>>(512); let (tx, mut rx) = broadcast::channel::<Arc<ProLabel>>(512);
let metrics = ProMetrics::new(); let metrics = ProMetrics::new();
let registration_cancellation = CancellationToken::new();
let rtx = tx.clone(); let rtx = tx.clone();
let ctk = registration_cancellation.clone();
let promt = metrics.clone(); let promt = metrics.clone();
let consumer_ct = ct.clone(); let consumer_ct = ct.clone();
joinset.spawn(async move { joinset.spawn(async move {
@ -178,7 +180,7 @@ fn init_pro_metrics(
rec_limit = 4; rec_limit = 4;
} }
Err(e) => { Err(e) => {
tracing::error!(k13t.no_report=true, debug=?e, "Failed to receive prometheus measurement from tracing"); tracing::error!(no_pro=true, debug=?e, "Failed to receive prometheus measurement from tracing");
rx = rtx.subscribe(); // dumb recover strategy; rx = rtx.subscribe(); // dumb recover strategy;
tokio::time::sleep(Duration::from_secs(3)).await; tokio::time::sleep(Duration::from_secs(3)).await;
rec_limit -= 1; rec_limit -= 1;
@ -187,17 +189,82 @@ fn init_pro_metrics(
} }
} }
} }
ctk.cancel();
tracing::error!( tracing::error!(
k13t.no_report=true, no_pro = true,
"Prometheus measurement from tracing stopped" "Prometheus measurement from tracing stopped"
); );
}); });
let registry = { let ctk = ct.clone();
joinset.spawn(async move {
let mut registry = Registry::default(); let mut registry = Registry::default();
loop {
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(10)) => {
metrics.register(&mut registry); metrics.register(&mut registry);
registry },
}; _ = registration_cancellation.cancelled() => {
tracing::warn!(no_pro=true, "Stopped publishing prometheus metrics: consumer job cancelled");
break;
}
_ = ctk.cancelled() => {
tracing::warn!(no_pro=true, "Stopped publishing prometheus metrics: shutdown");
break;
}
}
}
});
(tx, registry) tx
}
fn init_otel(joinset: &mut JoinSet<()>, ct: CancellationToken) -> anyhow::Result<Sender<Arc<OtelObservation>>> {
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::<Arc<OtelObservation>>(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!(
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!(no_otel = true, "OpenTelemetry reporting from tracing stopped");
});
Ok(tx)
} }

View file

@ -0,0 +1,106 @@
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<String>,
pub k8s_api: Option<String>,
pub k8s_uid: Option<String>,
pub trace_id: Option<String>,
}
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: <SdkLoggerProvider as LoggerProvider>::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();
}
}

View file

@ -4,6 +4,7 @@ use std::time::{
SystemTime, SystemTime,
}; };
use opentelemetry::trace::TraceContextExt;
use prometheus_client::encoding::{ use prometheus_client::encoding::{
EncodeLabelSet, EncodeLabelSet,
EncodeLabelValue, EncodeLabelValue,
@ -15,8 +16,9 @@ use prometheus_client::metrics::exemplar::{
}; };
use prometheus_client::metrics::family::Family; use prometheus_client::metrics::family::Family;
use prometheus_client::registry::Registry; use prometheus_client::registry::Registry;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use crate::reporting::span_trace_id; use crate::reporting::trace_id_str;
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct ProMetrics { pub struct ProMetrics {
@ -45,7 +47,6 @@ impl ProMetrics {
} }
/// Start measuring reconcile duration. Registers on `TimedMeasurer` drop /// Start measuring reconcile duration. Registers on `TimedMeasurer` drop
// TODO: use this
pub fn measure_reconcilation(&self) -> TimedMeasurer { pub fn measure_reconcilation(&self) -> TimedMeasurer {
self.reconcile_metrics.measure() self.reconcile_metrics.measure()
} }
@ -70,9 +71,8 @@ pub struct ProReconcileMetrics {
impl ProReconcileMetrics { impl ProReconcileMetrics {
pub fn measure(&self) -> TimedMeasurer { pub fn measure(&self) -> TimedMeasurer {
self.runs.inc(); self.runs.inc();
let label = span_trace_id().map(|id| TraceLabel { let trace_id = tracing::Span::current().context().span().span_context().trace_id();
trace_id: id.to_string(), let label = trace_id_str(&trace_id).map(|trace_id| TraceLabel { trace_id });
});
TimedMeasurer { TimedMeasurer {
label, label,

View file

@ -5,6 +5,7 @@ use kube::runtime::events::{
Event, Event,
EventType, EventType,
}; };
use opentelemetry::logs::Severity;
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use tracing::field::Visit; use tracing::field::Visit;
use tracing::span::Attributes; use tracing::span::Attributes;
@ -13,23 +14,31 @@ use tracing::{
Id, Id,
Subscriber, Subscriber,
}; };
use tracing_opentelemetry::OtelData;
use tracing_subscriber::Layer; use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context; use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::registry::LookupSpan;
use crate::reporting::k8s::K8sEvent; use crate::reporting::k8s_event::K8sEvent;
use crate::reporting::otel::{
OtelLogFields,
OtelObservation,
};
use crate::reporting::prometheus::{ use crate::reporting::prometheus::{
ProK8sExemplar, ProK8sExemplar,
ProK8sLabel, ProK8sLabel,
ProK8sSeverity, ProK8sSeverity,
ProLabel, ProLabel,
}; };
use crate::reporting::trace_id_str;
pub struct TracingK8sEventLayerConfig { pub struct TracingK8sEventLayerConfig {
/// Set to enable k8s events from tracing /// Set to enable k8s events from tracing
pub k8s_events: Option<Sender<Arc<K8sEvent>>>, pub k8s_events: Option<Sender<Arc<K8sEvent>>>,
/// set to enable prometheus metrics from tracing /// set to enable prometheus metrics from tracing
pub prometheus: Option<Sender<Arc<ProLabel>>>, pub prometheus: Option<Sender<Arc<ProLabel>>>,
/// set to enable otel observations from tracing
pub otel: Option<Sender<Arc<OtelObservation>>>,
} }
/// Watches for tracing events, containing information necessary to construst k8s events and write prometheus metrics. /// Watches for tracing events, containing information necessary to construst k8s events and write prometheus metrics.
@ -37,11 +46,10 @@ pub struct TracingK8sEventLayerConfig {
/// - k8s native events you must record yourself - thats what `tx` is for. /// - 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`. /// - prometheus metrics are written automatically (if metrics set), but labels are also send with `tx`.
/// - k8s events are rate-limited to 10 events/min /// - k8s events are rate-limited to 10 events/min
///
/// Recognizes fields with `k13t` (KubernatesEvent) prefix.
pub struct TracingK8sEventLayer { pub struct TracingK8sEventLayer {
k8s_tx: Option<Sender<Arc<K8sEvent>>>, k8s_tx: Option<Sender<Arc<K8sEvent>>>,
pro_tx: Option<Sender<Arc<ProLabel>>>, pro_tx: Option<Sender<Arc<ProLabel>>>,
otel_tx: Option<Sender<Arc<OtelObservation>>>,
} }
impl TracingK8sEventLayer { impl TracingK8sEventLayer {
@ -49,6 +57,7 @@ impl TracingK8sEventLayer {
Self { Self {
k8s_tx: config.k8s_events, k8s_tx: config.k8s_events,
pro_tx: config.prometheus, pro_tx: config.prometheus,
otel_tx: config.otel,
} }
} }
} }
@ -81,7 +90,6 @@ where
return; return;
} }
let span = ctx.event_span(event);
let scope = match ctx.event_scope(event) { let scope = match ctx.event_scope(event) {
Some(s) => s, Some(s) => s,
None => { None => {
@ -114,13 +122,11 @@ where
return; return;
} }
let trace_id = span // gather trace_id
.and_then(|s| { let otel_trace_id = ctx
s.extensions() .event_span(event)
.get::<opentelemetry::trace::SpanContext>() .and_then(|span| span.extensions().get::<OtelData>().and_then(|dat| dat.trace_id()));
.map(|ctx| ctx.trace_id()) let str_trace_id = otel_trace_id.as_ref().and_then(trace_id_str);
})
.map(|id| id.to_string());
// write k8s event // write k8s event
let k8s_event_type = match *event.metadata().level() { let k8s_event_type = match *event.metadata().level() {
@ -139,7 +145,7 @@ where
object: span_fields.to_object(), object: span_fields.to_object(),
event, event,
})) }))
.inspect_err(|e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to issue event on k8s event from tracing")); .inspect_err(|e| tracing::error!(debug=?e, "Failed to issue event on k8s event from tracing"));
} }
// write/measure pro event // write/measure pro event
@ -157,13 +163,48 @@ where
}; };
if event_fields.no_pro.is_none_or(|v| !v) if event_fields.no_pro.is_none_or(|v| !v)
&& span_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, trace_id) && let Some(label) = try_create_pro_label(&span_fields, &event_fields, pro_severity, str_trace_id.clone())
&& let Some(tx) = self.pro_tx.as_ref() && let Some(tx) = self.pro_tx.as_ref()
{ {
_ = tx.send(Arc::new(label)).inspect_err( _ = tx.send(Arc::new(label)).inspect_err(
|e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to issue event on prometheus label measure from tracing"), |e| tracing::error!(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"));
}
} }
} }
@ -248,22 +289,22 @@ impl SpanFields {
impl Visit for SpanFields { impl Visit for SpanFields {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) { fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
match field.name() { match field.name() {
"k13t.name" => self.name = Some(value.to_string()), "k8s_name" => self.name = Some(value.to_string()),
"k13t.ns" => self.ns = Some(value.to_string()), "k8s_ns" => self.ns = Some(value.to_string()),
"k13t.kind" => self.kind = Some(value.to_string()), "k8s_kind" => self.kind = Some(value.to_string()),
"k13t.api" => self.api = Some(value.to_string()), "k8s_api" => self.api = Some(value.to_string()),
"k13t.uid" => self.uid = Some(value.to_string()), "k8s_uid" => self.uid = Some(value.to_string()),
"k13t.version" => self.version = Some(value.to_string()), "k8s_version" => self.version = Some(value.to_string()),
"k13t.field_path" => self.field_path = Some(value.to_string()), "k8s_field_path" => self.field_path = Some(value.to_string()),
_ => {} _ => {}
} }
} }
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
match field.name() { match field.name() {
"k13t.no_event" => self.no_event = Some(value), "no_event" => self.no_event = Some(value),
"k13t.no_pro" => self.no_pro = Some(value), "no_pro" => self.no_pro = Some(value),
"k13t.no_otel" => self.no_otel = Some(value), "no_otel" => self.no_otel = Some(value),
_ => {} _ => {}
} }
} }
@ -273,14 +314,12 @@ impl Visit for SpanFields {
#[derive(Default, Clone)] #[derive(Default, Clone)]
struct EventFields { struct EventFields {
/// Setting this to true will prevent from emitting k8s events /// Setting this to true will block entire span from emitting k8s events
no_event: Option<bool>, no_event: Option<bool>,
/// Setting this to true will prevent from emitting prometheus metric labels /// Setting this to true will block entire span from emitting prometheus metric labels
no_pro: Option<bool>, no_pro: Option<bool>,
/// Setting this to true will prevent from emitting opentelemetry logs, /// Setting this to true will block entire span from emitting opentelemetry logs,
no_otel: Option<bool>, no_otel: Option<bool>,
/// Setting this to true will prevent from emitting any pro/otel/k8s events
no_report: Option<bool>,
/// Only for: /// Only for:
/// - prometheus label: force event severity to "critical". Only triggers on `ERROR` tracing level /// - 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 /// - opentelemetry: make event severify to "Fatal". Only triggers on `ERROR` tracing level
@ -293,7 +332,7 @@ struct EventFields {
impl EventFields { impl EventFields {
/// Scope independend custom field validation /// Scope independend custom field validation
fn check_fields(&self) -> bool { fn check_fields(&self) -> bool {
!self.no_report.is_some_and(|v| v) && self.reason.is_some() && self.action.is_some() self.reason.is_some() && self.action.is_some()
} }
/// Return `None` if hard-requirements on fields not met. /// Return `None` if hard-requirements on fields not met.
@ -312,20 +351,19 @@ impl EventFields {
impl Visit for EventFields { impl Visit for EventFields {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) { fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
match field.name() { match field.name() {
"k13t.reason" => self.reason = Some(value.to_string()), "reason" => self.reason = Some(value.to_string()),
"k13t.action" => self.action = Some(value.to_string()), "action" => self.action = Some(value.to_string()),
"k13t.message" => self.message = Some(value.to_string()), "message" => self.message = Some(value.to_string()),
_ => {} _ => {}
} }
} }
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
match field.name() { match field.name() {
"k13t.critical" => self.critical = Some(value), "critical" => self.critical = Some(value),
"k13t.no_event" => self.no_event = Some(value), "no_event" => self.no_event = Some(value),
"k13t.no_pro" => self.no_pro = Some(value), "no_pro" => self.no_pro = Some(value),
"k13t.no_otel" => self.no_otel = Some(value), "no_otel" => self.no_otel = Some(value),
"k13t.no_report" => self.no_report = Some(value),
_ => {} _ => {}
} }
} }

View file

@ -1,2 +0,0 @@
pub mod secret;
pub mod ssh;

View file

@ -1 +0,0 @@
pub mod v1alpha1;

View file

@ -1,354 +0,0 @@
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");
}
}

Some files were not shown because too many files have changed in this diff Show more