feat: display last key renewal attempt (#3839)

* feat: display last key renewal attempt

* feat: improve UI + add renew button + dev instance

* fix: nits

* chore: update ee ref

* fix: nit

* fix: tests
This commit is contained in:
HugoCasa
2024-05-29 16:28:00 +02:00
committed by GitHub
parent 6b2299f7a8
commit dc2d7e1c8f
12 changed files with 247 additions and 21 deletions
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, created_at FROM metrics WHERE id = $1 ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "360d13c0a776e794063c99acad935a424f79017f3769e1150faa87cbb91367a8"
}
+3 -2
View File
@@ -9020,9 +9020,9 @@ dependencies = [
[[package]]
name = "triomphe"
version = "0.1.12"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90"
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
dependencies = [
"serde",
"stable_deref_trait",
@@ -10008,6 +10008,7 @@ dependencies = [
"swc_ecma_ast",
"swc_ecma_parser",
"swc_ecma_visit",
"triomphe",
"wasm-bindgen",
"windmill-parser",
]
+3
View File
@@ -254,3 +254,6 @@ sysinfo = "0.30.12"
tikv-jemallocator = { version = "0.5" }
tikv-jemalloc-sys = { version = "^0.5" }
tikv-jemalloc-ctl = { version = "^0.5" }
# 0.1.12 broken (nested dependency of swc_common)
triomphe = "<0.1.12"
+1 -1
View File
@@ -1 +1 @@
553aa1d75498c9c6c07a1495a154478fb0bc064e
982774ea62ab95914bd0502aac8c5c9b5a91b5c2
@@ -16,6 +16,7 @@ serde-wasm-bindgen.workspace = true
[dependencies]
windmill-parser.workspace = true
swc_common.workspace = true
triomphe.workspace = true
swc_ecma_parser.workspace = true
swc_ecma_ast.workspace = true
swc_ecma_visit.workspace = true
+39
View File
@@ -766,6 +766,45 @@ paths:
schema:
type: string
/settings/latest_key_renewal_attempt:
get:
summary: get latest key renewal attempt
operationId: getLatestKeyRenewalAttempt
tags:
- setting
responses:
"200":
description: status
content:
application/json:
schema:
type: object
properties:
result:
type: string
attempted_at:
type: string
format: date-time
required:
- result
- attempted_at
nullable: true
/settings/renew_license_key:
post:
summary: renew license key
operationId: renewLicenseKey
tags:
- setting
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/saml/test_metadata:
post:
summary: test metadata
+63 -1
View File
@@ -42,7 +42,12 @@ pub fn global_service() -> Router {
)
.route("/test_smtp", post(test_email))
.route("/test_license_key", post(test_license_key))
.route("/send_stats", post(send_stats));
.route("/send_stats", post(send_stats))
.route(
"/latest_key_renewal_attempt",
get(get_latest_key_renewal_attempt),
)
.route("/renew_license_key", post(renew_license_key));
#[cfg(feature = "parquet")]
{
@@ -263,3 +268,60 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
Ok("Sent stats".to_string())
}
#[derive(serde::Serialize)]
pub struct KeyRenewalAttempt {
result: String,
attempted_at: chrono::DateTime<chrono::Utc>,
}
pub async fn get_latest_key_renewal_attempt(
Extension(db): Extension<DB>,
authed: ApiAuthed,
) -> JsonResult<Option<KeyRenewalAttempt>> {
require_super_admin(&db, &authed.email).await?;
let last_attempt = sqlx::query!(
"SELECT value, created_at FROM metrics WHERE id = $1 ORDER BY created_at DESC LIMIT 1",
"license_key_renewal"
)
.fetch_optional(&db)
.await?;
match last_attempt {
Some(last_attempt) => {
let last_attempt_result = serde_json::from_value::<String>(last_attempt.value)
.map_err(|e| {
error::Error::InternalErr(format!("Failed to parse last attempt: {}", e))
})?;
Ok(Json(Some(KeyRenewalAttempt {
result: last_attempt_result,
attempted_at: last_attempt.created_at,
})))
}
None => Ok(Json(None)),
}
}
#[cfg(not(feature = "enterprise"))]
pub async fn renew_license_key() -> Result<String> {
return Err(error::Error::BadRequest(
"License key renewal not available on community edition".to_string(),
));
}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(Extension(db): Extension<DB>, authed: ApiAuthed) -> Result<String> {
require_super_admin(&db, &authed.email).await?;
windmill_common::stats_ee::send_stats(&"manual".to_string(), &HTTP_CLIENT, &db).await?;
let result = windmill_common::ee::renew_license_key(&HTTP_CLIENT, &db).await;
if result != "success" {
return Err(error::Error::BadRequest(format!(
"Failed to renew license key: {}",
result
)));
} else {
return Ok("Renewed license key".to_string());
}
}
+6
View File
@@ -26,6 +26,12 @@ pub enum CriticalErrorChannel {}
pub async fn trigger_critical_error_channels(_error_message: String) {}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(_http_client: &reqwest::Client, _db: &crate::db::DB) -> String {
// Implementation is not open source
"".to_string()
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
// Implementation is not open source
@@ -25,6 +25,7 @@ pub const OBJECT_STORE_CACHE_CONFIG_SETTING: &str = "object_store_cache_config";
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
pub const DEV_INSTANCE_SETTING: &str = "dev_instance";
pub const ENV_SETTINGS: [&str; 50] = [
"DISABLE_NSJAIL",
@@ -13,10 +13,10 @@
import KeycloakSetting from './KeycloakSetting.svelte'
import Alert from './common/alert/Alert.svelte'
import { isCloudHosted } from '$lib/cloud'
import { capitalize } from '$lib/utils'
import { capitalize, classNames } from '$lib/utils'
import { enterpriseLicense } from '$lib/stores'
import CustomOauth from './CustomOauth.svelte'
import { AlertTriangle, Plus, X } from 'lucide-svelte'
import { AlertCircle, AlertTriangle, BadgeCheck, Info, Plus, X, BadgeX } from 'lucide-svelte'
import CustomSso from './CustomSso.svelte'
import AuthentikSetting from '$lib/components/AuthentikSetting.svelte'
import AutheliaSetting from '$lib/components/AutheliaSetting.svelte'
@@ -25,6 +25,7 @@
import Password from './Password.svelte'
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
import { fade } from 'svelte/transition'
import Popover from './Popover.svelte'
export let tab: string = 'Core'
export let hideTabs: boolean = false
@@ -35,6 +36,10 @@
let initialRequirePreexistingUserForOauth: boolean = false
let requirePreexistingUserForOauth: boolean = false
let ssoOrOauth: 'sso' | 'oauth' = 'sso'
let latestKeyRenewalAttempt: {
result: string
attempted_at: string
} | null = null
let serverConfig = {}
let initialValues: Record<string, any> = {}
@@ -79,6 +84,7 @@
if (values['base_url'] == undefined) {
values['base_url'] = 'http://localhost'
}
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
}
export async function saveSettings() {
@@ -144,7 +150,7 @@
try {
let i = parseInt(splitted[1])
let date = new Date(i * 1000)
return date.toDateString()
return date.toLocaleDateString()
} catch {}
}
return undefined
@@ -180,6 +186,17 @@
let clientName = ''
let licenseKeyChanged = false
export async function renewLicenseKey() {
try {
await SettingService.renewLicenseKey()
sendUserToast('Key renewal successful')
loadSettings()
} catch (err) {
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
throw err
}
}
</script>
<div class="pb-8">
@@ -477,22 +494,81 @@
requestBody: { license_key: values[setting.key] }
})
sendUserToast('Valid key')
}}>Test Key</Button
}}
>
</div>
{#if values[setting.key]?.length > 0}
{#if parseDate(values[setting.key])}
<span class="text-tertiary text-2xs"
>License key expires on {parseDate(values[setting.key])}</span
>
Test Key
</Button>
{#if $enterpriseLicense}
<Button on:click={renewLicenseKey} size="xs">Renew key</Button>
{/if}
{/if}
{#if licenseKeyChanged}
<div class="text-yellow-600"
>Refresh page after setting license key and saving to unlock all
features</div
>
{/if}
</div>
<div class="mt-1 flex flex-col gap-1 items-start">
{#if values[setting.key]?.length > 0}
{#if parseDate(values[setting.key])}
<div class="flex flex-row gap-1 items-center">
<Info size={12} class="text-tertiary" />
<span class="text-tertiary text-xs"
>License key expires on {parseDate(values[setting.key])}</span
>
</div>
{/if}
{/if}
{#if latestKeyRenewalAttempt}
{@const attemptedAt = new Date(
latestKeyRenewalAttempt.attempted_at
).toLocaleString()}
<div class="relative">
<Popover notClickable>
<div class="flex flex-row items-center gap-1">
{#if latestKeyRenewalAttempt.result === 'success'}
<BadgeCheck class="text-green-600" size={12} />
{:else}
<BadgeX class="text-red-600" size={12} />
{/if}
<span
class={classNames(
'text-xs',
latestKeyRenewalAttempt.result === 'success'
? 'text-green-600'
: 'text-red-600'
)}
>
{latestKeyRenewalAttempt.result === 'success'
? 'Latest key renewal succeeded'
: 'Latest key renewal failed'}
on {attemptedAt}
</span>
</div>
<div slot="text">
{#if latestKeyRenewalAttempt.result === 'success'}
<span class="text-green-300">
Latest key renewal succeeded on {attemptedAt}
</span>
{:else}
<span class="text-red-300">
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace(
'error: ',
''
)}
</span>
{/if}
<br />
As long as invoices are paid and usage corresponds to the subscription,
the key is renewed daily with a validity of 35 days (grace period).
</div>
</Popover>
</div>
{/if}
{#if licenseKeyChanged && !$enterpriseLicense}
<div class="flex flex-row items-center gap-1">
<AlertCircle size={12} class="text-yellow-600" />
<span class="text-xs text-yellow-600">
Refresh page after setting and saving license key to unlock all
features
</span>
</div>
{/if}
</div>
{:else if setting.fieldType == 'email'}
<input
type="email"
@@ -47,6 +47,7 @@
id="password"
type="password"
bind:value={password}
on:keydown
autocomplete="off"
{placeholder}
{disabled}
@@ -77,6 +77,14 @@ export const settings: Record<string, Setting[]> = {
placeholder: 'only needed to prepare upgrade to EE',
storage: 'setting'
},
{
label: 'Non-prod instance',
description: 'Whether we should consider the reported usage of this instance as non-prod',
key: 'dev_instance',
fieldType: 'boolean',
storage: 'setting',
ee_only: 'This is only relevant for EE'
},
{
label: 'Retention Period in secs',
key: 'retention_period_secs',