mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 08:03:06 +00:00
* fix: relock apps in place so imported script changes reach them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: requeue an app relock whose version was rewritten meanwhile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: relock every component of the version a superseded app relock requeues Also share raw-app runnable dependency extraction between the worker and the dependency-map rebuild, and take no-key-update row locks, which still order against a deploy's UPDATE without blocking app_script inserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: relock every component when an app relock runs on a newer version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: resolve a requeued app relock's components when it runs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: regenerate every lock when an app relock relocks every component Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
160 lines
6.2 KiB
Rust
160 lines
6.2 KiB
Rust
/*
|
|
* Author: Ruben Fiszel
|
|
* Copyright: Windmill Labs, Inc 2022
|
|
* This file and its contents are licensed under the AGPLv3 License.
|
|
* Please see the included NOTICE for copyright information and
|
|
* LICENSE-AGPL for a copy of the license.
|
|
*/
|
|
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
use serde_json::{from_value, Value};
|
|
|
|
use crate::{error, scripts::ScriptLang};
|
|
|
|
pub use windmill_types::apps::*;
|
|
|
|
lazy_static::lazy_static! {
|
|
pub static ref APP_WORKSPACED_ROUTE: AtomicBool = AtomicBool::new(false);
|
|
}
|
|
|
|
/// Whether the app value carries an `inlineScript` anywhere.
|
|
///
|
|
/// Deliberately not built on [`traverse_app_inline_scripts`], which only reports a script whose
|
|
/// `language` parses and stops descending as soon as it sees an `inlineScript` key. That is right
|
|
/// for locking (nothing to lock without a language) and wrong for an authorization check, where
|
|
/// the author picks the fields: this refuses the key itself, whatever it contains.
|
|
pub fn app_value_has_inline_script(value: &Value) -> bool {
|
|
match value {
|
|
Value::Object(object) => {
|
|
object.contains_key("inlineScript") || object.values().any(app_value_has_inline_script)
|
|
}
|
|
Value::Array(array) => array.iter().any(app_value_has_inline_script),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Every workspace runnable the app value points a component at, as `(is_flow, path)`.
|
|
///
|
|
/// This is what the deployed bundle actually asks `execute_component` to run: it resolves a
|
|
/// `runnable_id` against the stored `runnables` and sends `{runType}/{path}`. The policy's
|
|
/// triggerables are a separate surface, so both have to be authorized. Anything not plain text
|
|
/// is refused, never skipped: the editor compares `type` loosely (`["path"] == "path"`) and
|
|
/// interpolates `path` (`["u/x"]` → `u/x`), so a skipped entry still runs.
|
|
pub fn app_value_runnable_paths(value: &Value) -> error::Result<Vec<(bool, String)>> {
|
|
let bad = |msg: String| error::Error::BadRequest(msg);
|
|
let runnables = match value.get("runnables") {
|
|
None | Some(Value::Null) => return Ok(vec![]),
|
|
Some(Value::Object(runnables)) => runnables,
|
|
Some(_) => return Err(bad("App runnables must be an object".to_string())),
|
|
};
|
|
let mut out = Vec::new();
|
|
for (id, runnable) in runnables {
|
|
let Value::Object(runnable) = runnable else {
|
|
return Err(bad(format!("Runnable {id} must be an object")));
|
|
};
|
|
match runnable.get("type") {
|
|
Some(Value::String(t)) if t == "runnableByPath" || t == "path" => {}
|
|
None | Some(Value::String(_)) => continue,
|
|
Some(_) => return Err(bad(format!("Runnable {id} has a non-text type"))),
|
|
}
|
|
let Some(Value::String(path)) = runnable.get("path") else {
|
|
return Err(bad(format!("Runnable {id} must name its path as text")));
|
|
};
|
|
let is_flow = match runnable.get("runType") {
|
|
Some(Value::String(t)) if t == "flow" => true,
|
|
Some(Value::String(t)) if t == "script" || t == "hubscript" => false,
|
|
_ => return Err(bad(format!("Unsupported runType for runnable {path}"))),
|
|
};
|
|
out.push((is_flow, path.clone()));
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
pub const RAW_APP_RUNNABLES: &str = "runnables";
|
|
|
|
/// The `runnables` map of a raw app, when `value` is it. An inline script's dependency-map node
|
|
/// id is its closest enclosing component `id`; raw app runnables carry none, so each is keyed by
|
|
/// its name in this map instead, or a relock could not tell them apart.
|
|
pub fn raw_app_runnables_map<'a>(
|
|
key: &str,
|
|
value: &'a Value,
|
|
container_id: Option<&String>,
|
|
) -> Option<&'a serde_json::Map<String, Value>> {
|
|
match value {
|
|
Value::Object(runnables) if container_id.is_none() && key == RAW_APP_RUNNABLES => {
|
|
Some(runnables)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Traverse FlowValue while invoking provided by caller callback on leafs
|
|
// #[async_recursion::async_recursion(?Send)]
|
|
pub fn traverse_app_inline_scripts<
|
|
C: FnMut(AppInlineScript, Option<String>) -> error::Result<()>,
|
|
>(
|
|
value: &Value,
|
|
// Set to None.
|
|
container_id: Option<String>,
|
|
cb: &mut C,
|
|
) -> error::Result<()> {
|
|
match value {
|
|
Value::Object(object) => {
|
|
if let Some(Value::Object(script)) = object.get("inlineScript") {
|
|
let (language, lock, code) = (
|
|
script
|
|
.get("language")
|
|
.cloned()
|
|
.map(|v| from_value::<ScriptLang>(v).ok())
|
|
.flatten(),
|
|
script
|
|
.get("lock")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned),
|
|
script
|
|
.get("content")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned)
|
|
.ok_or(error::Error::internal_err(
|
|
"Missing `content` in inlineScript".to_string(),
|
|
))?,
|
|
);
|
|
if language.is_some() {
|
|
cb(
|
|
AppInlineScript { language, content: code.to_owned(), lock },
|
|
container_id.clone(),
|
|
)?;
|
|
}
|
|
} else {
|
|
for (key, value) in object {
|
|
if let Some(runnables) =
|
|
raw_app_runnables_map(key, value, container_id.as_ref())
|
|
{
|
|
for (id, runnable) in runnables {
|
|
traverse_app_inline_scripts(runnable, Some(id.clone()), cb)?;
|
|
}
|
|
continue;
|
|
}
|
|
traverse_app_inline_scripts(
|
|
value,
|
|
object
|
|
.get("id")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned)
|
|
.or(container_id.clone()),
|
|
cb,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
Value::Array(array) => {
|
|
for value in array {
|
|
traverse_app_inline_scripts(value, container_id.clone(), cb)?;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|