From dc2d7e1c8f5f7c50be04cfea6986ce03a3e33c7b Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Wed, 29 May 2024 16:28:00 +0200 Subject: [PATCH] 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 --- ...35a424f79017f3769e1150faa87cbb91367a8.json | 28 +++++ backend/Cargo.lock | 5 +- backend/Cargo.toml | 3 + backend/ee-repo-ref.txt | 2 +- backend/parsers/windmill-parser-ts/Cargo.toml | 1 + backend/windmill-api/openapi.yaml | 39 +++++++ backend/windmill-api/src/settings.rs | 64 +++++++++- backend/windmill-common/src/ee.rs | 6 + .../windmill-common/src/global_settings.rs | 1 + .../lib/components/InstanceSettings.svelte | 110 +++++++++++++++--- frontend/src/lib/components/Password.svelte | 1 + .../src/lib/components/instanceSettings.ts | 8 ++ 12 files changed, 247 insertions(+), 21 deletions(-) create mode 100644 backend/.sqlx/query-360d13c0a776e794063c99acad935a424f79017f3769e1150faa87cbb91367a8.json diff --git a/backend/.sqlx/query-360d13c0a776e794063c99acad935a424f79017f3769e1150faa87cbb91367a8.json b/backend/.sqlx/query-360d13c0a776e794063c99acad935a424f79017f3769e1150faa87cbb91367a8.json new file mode 100644 index 0000000000..c25a47f075 --- /dev/null +++ b/backend/.sqlx/query-360d13c0a776e794063c99acad935a424f79017f3769e1150faa87cbb91367a8.json @@ -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" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index e0b8394846..41584fc2e9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -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", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 6a32518364..b3240b2c02 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -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" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 3f723302af..96dd1a8a87 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -553aa1d75498c9c6c07a1495a154478fb0bc064e \ No newline at end of file +982774ea62ab95914bd0502aac8c5c9b5a91b5c2 \ No newline at end of file diff --git a/backend/parsers/windmill-parser-ts/Cargo.toml b/backend/parsers/windmill-parser-ts/Cargo.toml index 915f03b0ac..2a84c4e542 100644 --- a/backend/parsers/windmill-parser-ts/Cargo.toml +++ b/backend/parsers/windmill-parser-ts/Cargo.toml @@ -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 diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index b24ebbc058..8a873e6063 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -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 diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 199edc0833..10d2adb634 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -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, authed: ApiAuthed) -> Resu Ok("Sent stats".to_string()) } + +#[derive(serde::Serialize)] +pub struct KeyRenewalAttempt { + result: String, + attempted_at: chrono::DateTime, +} + +pub async fn get_latest_key_renewal_attempt( + Extension(db): Extension, + authed: ApiAuthed, +) -> JsonResult> { + 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::(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 { + 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, authed: ApiAuthed) -> Result { + 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()); + } +} diff --git a/backend/windmill-common/src/ee.rs b/backend/windmill-common/src/ee.rs index 5f2e656429..8c06a6456f 100644 --- a/backend/windmill-common/src/ee.rs +++ b/backend/windmill-common/src/ee.rs @@ -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 diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 335bb80813..1fcc0d0fdd 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -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", diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index fd3ca426fe..4569fcf341 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -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 = {} @@ -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 + } + }
@@ -477,22 +494,81 @@ requestBody: { license_key: values[setting.key] } }) sendUserToast('Valid key') - }}>Test Key -
- {#if values[setting.key]?.length > 0} - {#if parseDate(values[setting.key])} - License key expires on {parseDate(values[setting.key])} + Test Key + + {#if $enterpriseLicense} + {/if} - {/if} - {#if licenseKeyChanged} -
Refresh page after setting license key and saving to unlock all - features
- {/if} + +
+ {#if values[setting.key]?.length > 0} + {#if parseDate(values[setting.key])} +
+ + License key expires on {parseDate(values[setting.key])} +
+ {/if} + {/if} + {#if latestKeyRenewalAttempt} + {@const attemptedAt = new Date( + latestKeyRenewalAttempt.attempted_at + ).toLocaleString()} +
+ +
+ {#if latestKeyRenewalAttempt.result === 'success'} + + {:else} + + {/if} + + {latestKeyRenewalAttempt.result === 'success' + ? 'Latest key renewal succeeded' + : 'Latest key renewal failed'} + on {attemptedAt} + +
+
+ {#if latestKeyRenewalAttempt.result === 'success'} + + Latest key renewal succeeded on {attemptedAt} + + {:else} + + Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace( + 'error: ', + '' + )} + + {/if} +
+ 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). +
+
+
+ {/if} + {#if licenseKeyChanged && !$enterpriseLicense} +
+ + + Refresh page after setting and saving license key to unlock all + features + +
+ {/if} +
{:else if setting.fieldType == 'email'} = { 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',