mirror of
https://github.com/house-of-vanity/k8s-secrets.git
synced 2026-02-04 01:37:57 +00:00
Adjusted UI template. Added expiration for webhook secrets
This commit is contained in:
66
src/main.rs
66
src/main.rs
@@ -46,17 +46,29 @@ struct AppState {
|
||||
struct WebhookSecret {
|
||||
name: String,
|
||||
fields: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
expires: Option<String>,
|
||||
#[serde(skip_deserializing)]
|
||||
#[serde(default)]
|
||||
received_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Serialize)]
|
||||
enum SecretSource {
|
||||
Kubernetes,
|
||||
Webhook,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SecretData {
|
||||
name: String,
|
||||
data: Vec<(String, String)>,
|
||||
source: SecretSource,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
received_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
expires_at: Option<String>,
|
||||
expired: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -72,6 +84,49 @@ struct SecretQuery {
|
||||
field: String,
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> Option<chrono::Duration> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (num_str, unit) = if s.ends_with('m') {
|
||||
(&s[..s.len()-1], 'm')
|
||||
} else if s.ends_with('h') {
|
||||
(&s[..s.len()-1], 'h')
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let num: i64 = num_str.parse().ok()?;
|
||||
match unit {
|
||||
'm' => Some(chrono::Duration::minutes(num)),
|
||||
'h' => Some(chrono::Duration::hours(num)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_expiry(received_at: &str, expires: &Option<String>) -> (Option<String>, bool) {
|
||||
let duration = match expires {
|
||||
Some(exp) => match parse_duration(exp) {
|
||||
Some(d) => d,
|
||||
None => return (None, false),
|
||||
},
|
||||
None => return (None, false),
|
||||
};
|
||||
|
||||
let received = match chrono::NaiveDateTime::parse_from_str(received_at, "%Y-%m-%d %H:%M:%S UTC") {
|
||||
Ok(dt) => dt,
|
||||
Err(_) => return (None, false),
|
||||
};
|
||||
|
||||
let expires_at = received + duration;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let expired = now > expires_at;
|
||||
|
||||
(Some(expires_at.format("%Y-%m-%d %H:%M:%S UTC").to_string()), expired)
|
||||
}
|
||||
|
||||
async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
|
||||
let secrets_api: Api<Secret> = Api::namespaced(state.client.clone(), &state.namespace);
|
||||
let mut result = Vec::new();
|
||||
@@ -97,7 +152,10 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
|
||||
result.push(SecretData {
|
||||
name: secret_name.clone(),
|
||||
data: data_pairs,
|
||||
source: SecretSource::Kubernetes,
|
||||
received_at: None,
|
||||
expires_at: None,
|
||||
expired: false,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -105,7 +163,10 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
|
||||
result.push(SecretData {
|
||||
name: secret_name.clone(),
|
||||
data: vec![("error".to_string(), format!("Failed to read: {}", e))],
|
||||
source: SecretSource::Kubernetes,
|
||||
received_at: None,
|
||||
expires_at: None,
|
||||
expired: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,10 +202,15 @@ async fn index_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse
|
||||
.collect();
|
||||
data_pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let (expires_at, expired) = calculate_expiry(&webhook_secret.received_at, &webhook_secret.expires);
|
||||
|
||||
all_secrets.push(SecretData {
|
||||
name: webhook_secret.name.clone(),
|
||||
data: data_pairs,
|
||||
source: SecretSource::Webhook,
|
||||
received_at: Some(webhook_secret.received_at.clone()),
|
||||
expires_at,
|
||||
expired,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,50 @@
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
.secret.expired {
|
||||
border-color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
}
|
||||
.secret-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.secret-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.source-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.source-badge.k8s {
|
||||
background: #326ce5;
|
||||
color: white;
|
||||
}
|
||||
.source-badge.webhook {
|
||||
background: #6c5ce7;
|
||||
color: white;
|
||||
}
|
||||
.expired-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
.data-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -114,8 +152,21 @@
|
||||
</div>
|
||||
{% else %}
|
||||
{% for secret in secrets %}
|
||||
<div class="secret">
|
||||
<div class="secret-name">{{ secret.name }}</div>
|
||||
<div class="secret{% if secret.expired %} expired{% endif %}">
|
||||
<div class="secret-header">
|
||||
<div class="secret-name">{{ secret.name }}</div>
|
||||
{% match secret.source %}
|
||||
{% when SecretSource::Kubernetes %}
|
||||
<span class="source-badge k8s">☸ K8s</span>
|
||||
{% when SecretSource::Webhook %}
|
||||
<span class="source-badge webhook">⚡ Webhook</span>
|
||||
{% endmatch %}
|
||||
{% if secret.expired %}
|
||||
{% if let Some(expires) = secret.expires_at %}
|
||||
<span class="expired-badge">⚠ Expired at {{ expires }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for (key, value) in secret.data %}
|
||||
<div class="data-item">
|
||||
<span class="data-key">{{ key }}:</span>
|
||||
@@ -132,7 +183,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if let Some(received) = secret.received_at %}
|
||||
<div class="received-at">Received: {{ received }}</div>
|
||||
<div class="received-at">Received: {{ received }}{% if let Some(expires) = secret.expires_at %}{% if !secret.expired %} · Expires: {{ expires }}{% endif %}{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user