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 {
|
struct WebhookSecret {
|
||||||
name: String,
|
name: String,
|
||||||
fields: HashMap<String, String>,
|
fields: HashMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
|
expires: Option<String>,
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
received_at: String,
|
received_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Serialize)]
|
||||||
|
enum SecretSource {
|
||||||
|
Kubernetes,
|
||||||
|
Webhook,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct SecretData {
|
struct SecretData {
|
||||||
name: String,
|
name: String,
|
||||||
data: Vec<(String, String)>,
|
data: Vec<(String, String)>,
|
||||||
|
source: SecretSource,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
received_at: Option<String>,
|
received_at: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
expires_at: Option<String>,
|
||||||
|
expired: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@@ -72,6 +84,49 @@ struct SecretQuery {
|
|||||||
field: String,
|
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>> {
|
async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
|
||||||
let secrets_api: Api<Secret> = Api::namespaced(state.client.clone(), &state.namespace);
|
let secrets_api: Api<Secret> = Api::namespaced(state.client.clone(), &state.namespace);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
@@ -97,7 +152,10 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
|
|||||||
result.push(SecretData {
|
result.push(SecretData {
|
||||||
name: secret_name.clone(),
|
name: secret_name.clone(),
|
||||||
data: data_pairs,
|
data: data_pairs,
|
||||||
|
source: SecretSource::Kubernetes,
|
||||||
received_at: None,
|
received_at: None,
|
||||||
|
expires_at: None,
|
||||||
|
expired: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -105,7 +163,10 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
|
|||||||
result.push(SecretData {
|
result.push(SecretData {
|
||||||
name: secret_name.clone(),
|
name: secret_name.clone(),
|
||||||
data: vec![("error".to_string(), format!("Failed to read: {}", e))],
|
data: vec![("error".to_string(), format!("Failed to read: {}", e))],
|
||||||
|
source: SecretSource::Kubernetes,
|
||||||
received_at: None,
|
received_at: None,
|
||||||
|
expires_at: None,
|
||||||
|
expired: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,10 +202,15 @@ async fn index_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse
|
|||||||
.collect();
|
.collect();
|
||||||
data_pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
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 {
|
all_secrets.push(SecretData {
|
||||||
name: webhook_secret.name.clone(),
|
name: webhook_secret.name.clone(),
|
||||||
data: data_pairs,
|
data: data_pairs,
|
||||||
|
source: SecretSource::Webhook,
|
||||||
received_at: Some(webhook_secret.received_at.clone()),
|
received_at: Some(webhook_secret.received_at.clone()),
|
||||||
|
expires_at,
|
||||||
|
expired,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,50 @@
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-radius: 4px;
|
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 {
|
.secret-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 16px;
|
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 {
|
.data-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -114,8 +152,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for secret in secrets %}
|
{% for secret in secrets %}
|
||||||
<div class="secret">
|
<div class="secret{% if secret.expired %} expired{% endif %}">
|
||||||
<div class="secret-name">{{ secret.name }}</div>
|
<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 %}
|
{% for (key, value) in secret.data %}
|
||||||
<div class="data-item">
|
<div class="data-item">
|
||||||
<span class="data-key">{{ key }}:</span>
|
<span class="data-key">{{ key }}:</span>
|
||||||
@@ -132,7 +183,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if let Some(received) = secret.received_at %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user