Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e7748880e0 |
|||
|
|
3ac37809b7 |
||
|
|
e7f6ddf741 |
||
|
|
33bf3cbc20 |
||
|
|
53c25f81a4 |
||
|
|
02b46519dc |
||
|
|
67a930464a |
||
|
|
e15bb3bbc6 |
||
|
|
1e7d71ffc3 |
||
|
|
69ccd48c3b |
24 changed files with 1094 additions and 1032 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
.opencode
|
||||||
|
.envrc
|
||||||
1029
Cargo.lock
generated
1029
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
|
@ -18,18 +18,16 @@ 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_trace", "release_max_level_info"]
|
tracing.features = ["max_level_debug", "release_max_level_info"]
|
||||||
tracing-subscriber.version = "^0.3"
|
tracing-subscriber.version = "^0.3"
|
||||||
tracing-opentelemetry.version = "^0.32"
|
tracing-opentelemetry.version = "^0.33"
|
||||||
opentelemetry.version = "^0.31"
|
opentelemetry.version = "^0.32"
|
||||||
opentelemetry.features = ["trace"]
|
opentelemetry_sdk.version = "^0.32"
|
||||||
opentelemetry-otlp.version = "^0.31"
|
prometheus-client.version = "^0.24"
|
||||||
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"
|
||||||
|
|
@ -41,6 +39,8 @@ 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,7 +48,6 @@ 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
Normal file
185
README.md
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -11,13 +11,14 @@ publish.workspace = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
doc = false
|
doc = false
|
||||||
name = "generate_crds"
|
name = "crdgen"
|
||||||
path = "src/bin/generate_crds.rs"
|
path = "src/bin/crdgen.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
|
||||||
|
|
@ -25,7 +26,6 @@ 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,6 +38,7 @@ 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
|
||||||
|
|
@ -54,4 +55,5 @@ 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
|
||||||
|
|
|
||||||
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 +0,0 @@
|
||||||
fn main() {}
|
|
||||||
|
|
@ -30,10 +30,15 @@ pub fn bytes<G: CryptoRng>(rng: &mut G, size: usize) -> Vec<u8> {
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn str_bytes<G: CryptoRng>(rng: &mut G, length: usize, dictionary: &[char]) -> Result<Vec<u8>, GeneratorErr> {
|
pub fn str_bytes<G: CryptoRng>(
|
||||||
|
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);
|
||||||
|
|
||||||
let dictionary = Zeroizing::new(dictionary.iter().unique().cloned().collect::<Vec<char>>());
|
// TODO: unique may leak dictionary..?
|
||||||
|
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();
|
||||||
|
|
@ -65,12 +70,12 @@ pub fn str_bytes<G: CryptoRng>(rng: &mut G, length: usize, dictionary: &[char])
|
||||||
}
|
}
|
||||||
|
|
||||||
if buf.len() != length {
|
if buf.len() != length {
|
||||||
Err(GeneratorErr::RngStuck {
|
return 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
|
||||||
|
|
@ -194,11 +199,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).unwrap();
|
let gen0 = str_bytes(&mut rng, 0, dict.iter().cloned()).unwrap();
|
||||||
let gen1 = str_bytes(&mut rng, 1, &dict).unwrap();
|
let gen1 = str_bytes(&mut rng, 1, dict.iter().cloned()).unwrap();
|
||||||
let gen27 = str_bytes(&mut rng, 27, &dict).unwrap();
|
let gen27 = str_bytes(&mut rng, 27, dict.iter().cloned()).unwrap();
|
||||||
let gen256 = str_bytes(&mut rng, 256, &dict).unwrap();
|
let gen256 = str_bytes(&mut rng, 256, dict.iter().cloned()).unwrap();
|
||||||
assert_ne!(str_bytes(&mut rng, 256, &dict).unwrap(), gen256);
|
assert_ne!(str_bytes(&mut rng, 256, dict.iter().cloned()).unwrap(), gen256);
|
||||||
|
|
||||||
// length
|
// length
|
||||||
assert_eq!(gen0.len(), 0);
|
assert_eq!(gen0.len(), 0);
|
||||||
|
|
@ -222,7 +227,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).unwrap();
|
let gen16 = str_bytes(&mut rng, 16, dict.iter().cloned()).unwrap();
|
||||||
|
|
||||||
assert_eq!(gen16.len(), 16);
|
assert_eq!(gen16.len(), 16);
|
||||||
assert_dict_chars(&gen16, &dict);
|
assert_dict_chars(&gen16, &dict);
|
||||||
|
|
@ -235,8 +240,6 @@ 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
|
||||||
|
|
@ -258,12 +261,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).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()
|
str_bytes(&mut Hc128Rng::seed_from_u64(42), 256, dict.iter().cloned()).unwrap()
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
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(43), 256, &dict).unwrap()
|
str_bytes(&mut Hc128Rng::seed_from_u64(43), 256, dict.iter().cloned()).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
#![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;
|
||||||
|
|
|
||||||
36
kuyaseg-controller/src/reporting/k8s.rs
Normal file
36
kuyaseg-controller/src/reporting/k8s.rs
Normal 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!(k13t.no_report=true, debug=?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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ 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,
|
||||||
|
|
@ -11,19 +12,27 @@ 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_event::K8sEvent;
|
use crate::reporting::k8s::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_event;
|
pub mod k8s;
|
||||||
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")]
|
||||||
|
|
@ -32,24 +41,16 @@ 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()
|
||||||
|
|
@ -61,21 +62,18 @@ 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 = config
|
let (pro_tx, pro_registry) = config
|
||||||
.prometheus
|
.prometheus
|
||||||
.then(|| init_pro_metrics(&mut reporting_jobs, ct.clone()));
|
.then(|| init_pro_metrics(&mut reporting_jobs, ct.clone()))
|
||||||
let otel_tx = config
|
.unzip();
|
||||||
.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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +83,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
|
||||||
Reporter,
|
Reporter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::reporting::k8s_event::K8sEventRecorder;
|
use crate::reporting::k8s::K8sEventRecorder;
|
||||||
|
|
||||||
const WARN_THRESHOLD: usize = 10;
|
const WARN_THRESHOLD: usize = 10;
|
||||||
const MAX_CAPACITY: usize = 100;
|
const MAX_CAPACITY: usize = 100;
|
||||||
|
|
@ -126,7 +124,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!(no_event=true, queue_len=queue.len(), "K8s event queue exceeded threshold");
|
tracing::warn!(k13t.no_report=true, debug=?queue.len(), "K8s event queue exceeded threshold");
|
||||||
warned = true;
|
warned = true;
|
||||||
} else if queue.len() <= WARN_THRESHOLD {
|
} else if queue.len() <= WARN_THRESHOLD {
|
||||||
warned = false;
|
warned = false;
|
||||||
|
|
@ -135,7 +133,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!(no_event=true, debug=?e, "Failed to receive k8s event from tracing");
|
tracing::error!(k13t.no_report=true, debug=?e, "Failed to receive k8s event from tracing");
|
||||||
rx = rtx.subscribe(); // dumb recover strategy
|
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;
|
||||||
|
|
@ -145,7 +143,7 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
no_event = true,
|
k13t.no_report = true,
|
||||||
"K8s Event reporting from tracing stopped"
|
"K8s Event reporting from tracing stopped"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -153,7 +151,10 @@ fn init_k8s_events(client: kube::Client, joinset: &mut JoinSet<()>, ct: Cancella
|
||||||
tx
|
tx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender<Arc<ProLabel>> {
|
fn init_pro_metrics(
|
||||||
|
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;
|
||||||
|
|
@ -161,10 +162,7 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender<
|
||||||
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 {
|
||||||
|
|
@ -180,7 +178,7 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender<
|
||||||
rec_limit = 4;
|
rec_limit = 4;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(no_pro=true, debug=?e, "Failed to receive prometheus measurement from tracing");
|
tracing::error!(k13t.no_report=true, debug=?e, "Failed to receive prometheus measurement from tracing");
|
||||||
rx = rtx.subscribe(); // dumb recover strategy;
|
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;
|
||||||
|
|
@ -189,82 +187,17 @@ fn init_pro_metrics(joinset: &mut JoinSet<()>, ct: CancellationToken) -> Sender<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctk.cancel();
|
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
no_pro = true,
|
k13t.no_report=true,
|
||||||
"Prometheus measurement from tracing stopped"
|
"Prometheus measurement from tracing stopped"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let ctk = ct.clone();
|
let registry = {
|
||||||
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
|
(tx, registry)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use opentelemetry::logs::{
|
|
||||||
AnyValue,
|
|
||||||
LogRecord,
|
|
||||||
Logger,
|
|
||||||
LoggerProvider,
|
|
||||||
Severity,
|
|
||||||
};
|
|
||||||
use opentelemetry_sdk::logs::SdkLoggerProvider;
|
|
||||||
|
|
||||||
pub struct OtelLogFields {
|
|
||||||
pub severity: Severity,
|
|
||||||
pub message: String,
|
|
||||||
pub reason: String,
|
|
||||||
pub action: String,
|
|
||||||
pub k8s_name: String,
|
|
||||||
pub k8s_kind: String,
|
|
||||||
pub k8s_ns: Option<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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ use std::time::{
|
||||||
SystemTime,
|
SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use opentelemetry::trace::TraceContextExt;
|
|
||||||
use prometheus_client::encoding::{
|
use prometheus_client::encoding::{
|
||||||
EncodeLabelSet,
|
EncodeLabelSet,
|
||||||
EncodeLabelValue,
|
EncodeLabelValue,
|
||||||
|
|
@ -16,9 +15,8 @@ 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::trace_id_str;
|
use crate::reporting::span_trace_id;
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct ProMetrics {
|
pub struct ProMetrics {
|
||||||
|
|
@ -47,6 +45,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +70,9 @@ pub struct ProReconcileMetrics {
|
||||||
impl ProReconcileMetrics {
|
impl ProReconcileMetrics {
|
||||||
pub fn measure(&self) -> TimedMeasurer {
|
pub fn measure(&self) -> TimedMeasurer {
|
||||||
self.runs.inc();
|
self.runs.inc();
|
||||||
let trace_id = tracing::Span::current().context().span().span_context().trace_id();
|
let label = span_trace_id().map(|id| TraceLabel {
|
||||||
let label = trace_id_str(&trace_id).map(|trace_id| TraceLabel { trace_id });
|
trace_id: id.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
TimedMeasurer {
|
TimedMeasurer {
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ 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;
|
||||||
|
|
@ -14,31 +13,23 @@ 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_event::K8sEvent;
|
use crate::reporting::k8s::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.
|
||||||
|
|
@ -46,10 +37,11 @@ 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 {
|
||||||
|
|
@ -57,7 +49,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +81,7 @@ 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 => {
|
||||||
|
|
@ -122,11 +114,13 @@ where
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// gather trace_id
|
let trace_id = span
|
||||||
let otel_trace_id = ctx
|
.and_then(|s| {
|
||||||
.event_span(event)
|
s.extensions()
|
||||||
.and_then(|span| span.extensions().get::<OtelData>().and_then(|dat| dat.trace_id()));
|
.get::<opentelemetry::trace::SpanContext>()
|
||||||
let str_trace_id = otel_trace_id.as_ref().and_then(trace_id_str);
|
.map(|ctx| ctx.trace_id())
|
||||||
|
})
|
||||||
|
.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() {
|
||||||
|
|
@ -145,7 +139,7 @@ where
|
||||||
object: span_fields.to_object(),
|
object: span_fields.to_object(),
|
||||||
event,
|
event,
|
||||||
}))
|
}))
|
||||||
.inspect_err(|e| tracing::error!(debug=?e, "Failed to issue event on k8s event from tracing"));
|
.inspect_err(|e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to issue event on k8s event from tracing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// write/measure pro event
|
// write/measure pro event
|
||||||
|
|
@ -163,48 +157,13 @@ 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, str_trace_id.clone())
|
&& let Some(label) = try_create_pro_label(&span_fields, &event_fields, pro_severity, trace_id)
|
||||||
&& let Some(tx) = self.pro_tx.as_ref()
|
&& 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!(debug=?e, "Failed to issue event on prometheus label measure from tracing"),
|
|e| tracing::error!(k13t.no_report=true, debug=?e, "Failed to issue event on prometheus label measure from tracing"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write otel event
|
|
||||||
let otel_severity = match *event.metadata().level() {
|
|
||||||
tracing::Level::ERROR => {
|
|
||||||
if event_fields.critical.is_some_and(|v| v) {
|
|
||||||
Severity::Fatal
|
|
||||||
} else {
|
|
||||||
Severity::Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::Level::WARN => Severity::Warn,
|
|
||||||
tracing::Level::INFO => Severity::Info,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
if event_fields.no_otel.is_none_or(|v| !v)
|
|
||||||
&& span_fields.no_otel.is_none_or(|v| !v)
|
|
||||||
&& let Some(tx) = self.otel_tx.as_ref()
|
|
||||||
{
|
|
||||||
_ = tx
|
|
||||||
.send(Arc::new(OtelObservation::Log {
|
|
||||||
fields: OtelLogFields {
|
|
||||||
severity: otel_severity,
|
|
||||||
message: event_fields.message.clone().unwrap_or_default(),
|
|
||||||
reason: event_fields.reason.clone().unwrap_or_default(),
|
|
||||||
action: event_fields.action.clone().unwrap_or_default(),
|
|
||||||
k8s_name: span_fields.name.clone().unwrap_or_default(),
|
|
||||||
k8s_kind: span_fields.kind.clone().unwrap_or_default(),
|
|
||||||
k8s_ns: span_fields.ns.clone(),
|
|
||||||
k8s_api: span_fields.api.clone(),
|
|
||||||
k8s_uid: span_fields.uid.clone(),
|
|
||||||
trace_id: str_trace_id.clone(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.inspect_err(|e| tracing::error!(debug=?e, "Failed to issue otel observation from tracing"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,22 +248,22 @@ impl SpanFields {
|
||||||
impl Visit for SpanFields {
|
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() {
|
||||||
"k8s_name" => self.name = Some(value.to_string()),
|
"k13t.name" => self.name = Some(value.to_string()),
|
||||||
"k8s_ns" => self.ns = Some(value.to_string()),
|
"k13t.ns" => self.ns = Some(value.to_string()),
|
||||||
"k8s_kind" => self.kind = Some(value.to_string()),
|
"k13t.kind" => self.kind = Some(value.to_string()),
|
||||||
"k8s_api" => self.api = Some(value.to_string()),
|
"k13t.api" => self.api = Some(value.to_string()),
|
||||||
"k8s_uid" => self.uid = Some(value.to_string()),
|
"k13t.uid" => self.uid = Some(value.to_string()),
|
||||||
"k8s_version" => self.version = Some(value.to_string()),
|
"k13t.version" => self.version = Some(value.to_string()),
|
||||||
"k8s_field_path" => self.field_path = Some(value.to_string()),
|
"k13t.field_path" => self.field_path = Some(value.to_string()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
||||||
match field.name() {
|
match field.name() {
|
||||||
"no_event" => self.no_event = Some(value),
|
"k13t.no_event" => self.no_event = Some(value),
|
||||||
"no_pro" => self.no_pro = Some(value),
|
"k13t.no_pro" => self.no_pro = Some(value),
|
||||||
"no_otel" => self.no_otel = Some(value),
|
"k13t.no_otel" => self.no_otel = Some(value),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -314,12 +273,14 @@ impl Visit for SpanFields {
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
struct EventFields {
|
struct EventFields {
|
||||||
/// Setting this to true will block entire span from emitting k8s events
|
/// Setting this to true will prevent from emitting k8s events
|
||||||
no_event: Option<bool>,
|
no_event: Option<bool>,
|
||||||
/// Setting this to true will block entire span from emitting prometheus metric labels
|
/// Setting this to true will prevent from emitting prometheus metric labels
|
||||||
no_pro: Option<bool>,
|
no_pro: Option<bool>,
|
||||||
/// Setting this to true will block entire span from emitting opentelemetry logs,
|
/// Setting this to true will prevent from emitting opentelemetry logs,
|
||||||
no_otel: Option<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
|
||||||
|
|
@ -332,7 +293,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.reason.is_some() && self.action.is_some()
|
!self.no_report.is_some_and(|v| v) && self.reason.is_some() && self.action.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `None` if hard-requirements on fields not met.
|
/// Return `None` if hard-requirements on fields not met.
|
||||||
|
|
@ -351,19 +312,20 @@ 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() {
|
||||||
"reason" => self.reason = Some(value.to_string()),
|
"k13t.reason" => self.reason = Some(value.to_string()),
|
||||||
"action" => self.action = Some(value.to_string()),
|
"k13t.action" => self.action = Some(value.to_string()),
|
||||||
"message" => self.message = Some(value.to_string()),
|
"k13t.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() {
|
||||||
"critical" => self.critical = Some(value),
|
"k13t.critical" => self.critical = Some(value),
|
||||||
"no_event" => self.no_event = Some(value),
|
"k13t.no_event" => self.no_event = Some(value),
|
||||||
"no_pro" => self.no_pro = Some(value),
|
"k13t.no_pro" => self.no_pro = Some(value),
|
||||||
"no_otel" => self.no_otel = Some(value),
|
"k13t.no_otel" => self.no_otel = Some(value),
|
||||||
|
"k13t.no_report" => self.no_report = Some(value),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
kuyaseg-controller/src/resource/generated/mod.rs
Normal file
2
kuyaseg-controller/src/resource/generated/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod secret;
|
||||||
|
pub mod ssh;
|
||||||
1
kuyaseg-controller/src/resource/generated/secret/mod.rs
Normal file
1
kuyaseg-controller/src/resource/generated/secret/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod v1alpha1;
|
||||||
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
kuyaseg-controller/src/resource/generated/ssh/mod.rs
Normal file
1
kuyaseg-controller/src/resource/generated/ssh/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue