blob: 7470d34ffbb73fd47592b92f472d3d9cf010ffb2 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use maka_plugin_core::deployment::management::{
InstanceConfigure, InstanceSettingsPage, InstanceSettingsRead, SettingChange, SettingField,
SettingKind, SettingValue,
};
use std::collections::BTreeSet;
impl Manager {
pub(super) async fn read_settings(&self, input: Value) -> Result<Value> {
let request: InstanceSettingsRead =
serde_json::from_value(input).map_err(|e| e.to_string())?;
if request.source.state_domain != self.source.state_domain
|| request.source.deployment != self.source.deployment
{
return Err("foreign managed deployment source".into());
}
check_path(&request.path, true)?;
let _read = self.writes.lock().await;
let loaded = self
.store
.load_application_at(&request.source)
.await
.map_err(|e| e.to_string())?
.ok_or("managed source not found")?;
let plan = loaded.plan();
let instance = plan
.instances()
.get(&request.instance)
.ok_or("managed instance unavailable")?;
let schema = &instance
.entry
.as_ref()
.ok_or("open an executable child to configure it")?
.configuration;
let (schema, current) = at(schema, &instance.configuration, &request.path)?;
let mut fields = fields(
schema,
&instance.entry.as_ref().unwrap().configuration,
current,
)?;
if let Some(after) = &request.after {
let index = fields
.iter()
.position(|f| &f.key == after)
.ok_or("settings cursor unavailable")?;
fields.drain(..=index);
}
let (fields, next) = settings_page(fields)?;
serde_json::to_value(InstanceSettingsPage {
source: request.source,
instance: request.instance,
path: request.path,
fields,
next,
})
.map_err(|e| e.to_string())
}
pub(super) async fn configure(&self, input: Value) -> Result<Value> {
let request: InstanceConfigure =
serde_json::from_value(input).map_err(|e| e.to_string())?;
let _write = self.writes.lock().await;
let metadata = self.metadata().await?;
if request.expected_revision != metadata.source.reference.revision {
return conflict(metadata.source.reference.revision);
}
let loaded = self
.store
.load_application_at(&metadata.source.reference)
.await
.map_err(|e| e.to_string())?
.ok_or("managed source not found")?;
let (mut source, packages, plan, _) = loaded.into_parts();
let Some(instance) = plan
.instances()
.get(&request.instance)
.filter(|i| i.entry.is_some())
else {
return rejected(ManagementRejection::InstanceUnavailable);
};
let mut configuration = instance.configuration.clone();
let changes = match apply(&mut configuration, &request.changes) {
Ok(changes) => changes,
Err(error) => return rejected_diagnostic(ManagementRejection::InvalidSource, error),
};
let [root, path @ ..] = instance.path.as_slice() else {
return Err("invalid managed path".into());
};
let node = source
.input
.instances
.get_mut(root)
.ok_or("managed root disappeared")?;
if path.is_empty() {
for key in changes {
node.configuration.parameters.remove(&key);
match configuration.get(&key) {
Some(value) => {
node.configuration.values.insert(key, value.clone());
}
None => {
node.configuration.values.remove(&key);
}
}
}
} else {
// Existing composition overrides replace top-level fields. Editing a nested
// leaf freezes only that containing field, not any unrelated binding or value.
if changes.iter().any(|key| configuration.get(key).is_none()) {
return rejected_diagnostic(
ManagementRejection::InvalidSource,
"an inherited field cannot be removed from this instance".into(),
);
}
let index = node.overrides.iter().position(|patch| patch.path == path);
let patch = if let Some(index) = index {
&mut node.overrides[index]
} else {
node.overrides
.push(maka_plugin_core::composition::Override {
path: path.to_vec(),
..Default::default()
});
node.overrides.last_mut().unwrap()
};
for key in changes {
patch
.configuration
.insert(key.clone(), configuration[&key].clone());
}
}
self.commit(
request.expected_revision,
source.input,
source.remote_exports,
packages,
|source| source,
)
.await
}
}
// Local $refs can repeat a large enum across many fields. Bound the encoded
// projection as well as its row count, leaving room for source/path metadata.
pub(super) fn settings_page(
mut fields: Vec<SettingField>,
) -> Result<(Vec<SettingField>, Option<String>)> {
let mut bytes = 0;
let mut count = 0;
for field in fields.iter().take(8) {
let size = serde_json::to_vec(field).map_err(|e| e.to_string())?.len();
if count > 0 && size > (448usize * 1024).saturating_sub(bytes) {
break;
}
bytes = bytes
.checked_add(size)
.ok_or("settings page length overflow")?;
count += 1;
}
let more = fields.len() > count;
fields.truncate(count);
let next = more.then(|| fields.last().unwrap().key.clone());
Ok((fields, next))
}
pub(super) fn check_path(path: &[String], root: bool) -> Result<()> {
if !root && path.is_empty() {
return Err("invalid settings path".into());
}
Ok(())
}
fn scalar(value: &Value) -> Option<SettingValue> {
Some(match value {
Value::String(text) => SettingValue::Text { text: text.clone() },
Value::Number(number) => SettingValue::Number {
text: number.to_string(),
},
Value::Bool(value) => SettingValue::Boolean { value: *value },
Value::Null => SettingValue::Null,
_ => return None,
})
}
fn json(value: &SettingValue) -> Result<Value> {
Ok(match value {
SettingValue::Text { text } => Value::String(text.clone()),
SettingValue::Number { text } => {
let value: Value =
serde_json::from_str(text).map_err(|e| format!("invalid number: {e}"))?;
if !value.is_number() {
return Err("number required".into());
}
value
}
SettingValue::Boolean { value } => Value::Bool(*value),
SettingValue::Null => Value::Null,
})
}
pub(super) fn apply(
configuration: &mut Value,
changes: &[SettingChange],
) -> Result<BTreeSet<String>> {
if changes.is_empty() {
return Err("settings changes must not be empty".into());
}
let mut touched = BTreeSet::new();
let mut paths = BTreeSet::new();
for change in changes {
check_path(&change.path, false)?;
if !paths.insert(change.path.clone()) {
return Err("duplicate setting change".into());
}
touched.insert(change.path[0].clone());
let (key, parents) = change.path.split_last().unwrap();
let mut current = &mut *configuration;
for part in parents {
current = match current {
Value::Object(map) => {
if change.value.is_some() {
map.entry(part)
.or_insert_with(|| Value::Object(Default::default()))
} else {
map.get_mut(part).ok_or("settings parent unavailable")?
}
}
Value::Array(items) => items
.get_mut(index(part)?)
.ok_or("settings item unavailable")?,
_ => return Err("settings parent is not a group".into()),
};
}
let value = change.value.as_ref().map(json).transpose()?;
match current {
Value::Object(map) => {
if let Some(value) = value {
map.insert(key.clone(), value);
} else {
map.remove(key);
}
}
Value::Array(items) => {
let index = index(key)?;
if index >= items.len() {
return Err("settings item unavailable".into());
}
if let Some(value) = value {
items[index] = value;
} else {
return Err("array membership must be changed as a group".into());
}
}
_ => return Err("settings parent is not a group".into()),
}
}
Ok(touched)
}
fn index(key: &str) -> Result<usize> {
let index = key.parse::<usize>().map_err(|_| "invalid settings index")?;
if index.to_string() != key {
return Err("invalid settings index".into());
}
Ok(index)
}
// Schema is a display hint; admission validates the entire resulting source.
// Resolve only local references and the branch selected by the actual value.
pub(super) fn resolved<'a>(
mut schema: &'a Value,
root: &'a Value,
value: Option<&Value>,
) -> &'a Value {
let mut visited = BTreeSet::new();
while visited.insert(std::ptr::from_ref(schema)) {
if let Some(reference) = schema
.get("$ref")
.and_then(Value::as_str)
.and_then(|r| r.strip_prefix('#'))
.and_then(|r| root.pointer(r))
{
schema = reference;
} else if let Some(branches) = schema
.get("oneOf")
.or_else(|| schema.get("anyOf"))
.and_then(Value::as_array)
{
let Some(branch) = branches
.iter()
.find(|branch| matches_value(branch, root, value))
else {
break;
};
schema = branch;
} else {
break;
}
}
schema
}
fn matches_value(schema: &Value, root: &Value, value: Option<&Value>) -> bool {
let schema = if let Some(reference) = schema
.get("$ref")
.and_then(Value::as_str)
.and_then(|r| r.strip_prefix('#'))
.and_then(|r| root.pointer(r))
{
reference
} else {
schema
};
if let Some(value) = value {
if let Some(constant) = schema.get("const") {
return value == constant;
}
if let Some(properties) = schema.get("properties").and_then(Value::as_object)
&& properties.iter().any(|(key, field)| {
field
.get("const")
.is_some_and(|v| value.get(key) != Some(v))
})
{
return false;
}
match schema.get("type").and_then(Value::as_str) {
Some("null") => value.is_null(),
Some("object") => value.is_object(),
Some("array") => value.is_array(),
Some("string") => value.is_string(),
Some("boolean") => value.is_boolean(),
Some("number" | "integer") => value.is_number(),
_ => true,
}
} else {
schema.get("type").and_then(Value::as_str) != Some("null")
}
}
pub(super) fn at<'a>(
root: &'a Value,
configuration: &'a Value,
path: &[String],
) -> Result<(&'a Value, Option<&'a Value>)> {
let mut schema = root;
let mut value = Some(configuration);
for part in path {
schema = resolved(schema, root, value);
if value.is_some_and(Value::is_array) {
let i = index(part)?;
value = value.and_then(|v| v.get(i));
schema = schema
.get("prefixItems")
.and_then(|v| v.get(i))
.or_else(|| schema.get("items"))
.unwrap_or(&Value::Null);
} else {
value = value.and_then(|v| v.get(part));
schema = schema
.get("properties")
.and_then(|p| p.get(part))
.unwrap_or(&Value::Null);
}
if value.is_none() && schema.is_null() {
return Err("settings path unavailable".into());
}
}
Ok((resolved(schema, root, value), value))
}
pub(super) fn fields(
schema: &Value,
root: &Value,
value: Option<&Value>,
) -> Result<Vec<SettingField>> {
if let Some(items) = value.and_then(Value::as_array) {
return Ok(items
.iter()
.enumerate()
.map(|(i, value)| {
let item = schema
.get("prefixItems")
.and_then(|v| v.get(i))
.or_else(|| schema.get("items"))
.unwrap_or(&Value::Null);
field(i.to_string(), item, root, Some(value), true)
})
.collect());
}
if !value.is_none_or(Value::is_object) {
return Err("settings path is not a group".into());
}
let properties = schema.get("properties").and_then(Value::as_object);
let values = value.and_then(Value::as_object);
let keys: BTreeSet<_> = properties
.into_iter()
.flat_map(|v| v.keys())
.chain(values.into_iter().flat_map(|v| v.keys()))
.collect();
Ok(keys
.into_iter()
.map(|key| {
let required = schema
.get("required")
.and_then(Value::as_array)
.is_some_and(|v| v.iter().any(|v| v.as_str() == Some(key)));
field(
key.clone(),
properties.and_then(|p| p.get(key)).unwrap_or(&Value::Null),
root,
values.and_then(|v| v.get(key)),
required,
)
})
.collect())
}
fn field(
key: String,
schema: &Value,
root: &Value,
value: Option<&Value>,
required: bool,
) -> SettingField {
let original = schema;
let schema = resolved(schema, root, value.filter(|value| !value.is_null()));
let kind = match value {
Some(Value::Object(_)) => SettingKind::Object,
Some(Value::Array(_)) => SettingKind::Array,
Some(Value::String(_)) => SettingKind::Text,
Some(Value::Number(_)) => SettingKind::Number,
Some(Value::Bool(_)) => SettingKind::Boolean,
_ => match schema.get("type").and_then(Value::as_str) {
Some("object") => SettingKind::Object,
Some("array") => SettingKind::Array,
Some("string") => SettingKind::Text,
Some("number" | "integer") => SettingKind::Number,
Some("boolean") => SettingKind::Boolean,
Some("null") => SettingKind::Null,
_ => SettingKind::Unsupported,
},
};
let mut options: Vec<SettingValue> = schema
.get("enum")
.and_then(Value::as_array)
.map(|v| v.iter().filter_map(scalar).collect())
.unwrap_or_default();
let label = original
.get("title")
.or_else(|| schema.get("title"))
.and_then(Value::as_str)
.unwrap_or(&key)
.to_owned();
let description = original
.get("description")
.or_else(|| schema.get("description"))
.and_then(Value::as_str)
.unwrap_or("")
.to_owned();
let nullable = value.is_some_and(Value::is_null)
|| schema.get("type").and_then(Value::as_str) == Some("null")
|| original
.get("type")
.and_then(Value::as_array)
.is_some_and(|v| v.iter().any(|v| v == "null"))
|| original
.get("anyOf")
.and_then(Value::as_array)
.is_some_and(|v| v.iter().any(|v| v.get("type").is_some_and(|v| v == "null")));
let private = [original, schema].iter().any(|schema| {
schema.get("writeOnly") == Some(&Value::Bool(true))
|| schema.get("format").and_then(Value::as_str) == Some("password")
});
let scalar = if private {
options.clear();
None
} else {
value.and_then(scalar)
};
let editable = !private
&& [original, schema]
.iter()
.all(|schema| schema.get("readOnly") != Some(&Value::Bool(true)))
&& !schema.as_bool().is_some_and(|v| !v)
&& !schema.as_object().is_some_and(|v| v.contains_key("const"))
&& !matches!(kind, SettingKind::Unsupported)
&& (value.is_none()
|| scalar.is_some()
|| value.is_some_and(|v| v.is_object() || v.is_array()));
SettingField {
key,
label,
description,
kind,
present: value.is_some(),
required,
nullable,
editable,
value: scalar,
options,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn referenced_fields_keep_private_and_read_only_annotations() {
let root = json!({"$defs":{"text":{"type":"string","enum":["private"]}}});
for annotation in [json!({"writeOnly":true}), json!({"format":"password"})] {
let mut schema = json!({"$ref":"#/$defs/text"});
schema
.as_object_mut()
.unwrap()
.extend(annotation.as_object().unwrap().clone());
let field = field(
"credential".into(),
&schema,
&root,
Some(&json!("private")),
true,
);
assert!(field.value.is_none());
assert!(field.options.is_empty());
assert!(!field.editable);
}
let field = field(
"id".into(),
&json!({"$ref":"#/$defs/text","readOnly":true}),
&root,
Some(&json!("original")),
true,
);
assert!(!field.editable);
}
#[test]
fn repeated_large_enums_page_by_encoded_bytes_without_losing_a_field() {
let options: Vec<_> = (0..16)
.map(|i| json!(format!("{i}{}", "界".repeat(4000))))
.collect();
let root = json!({"$defs":{"choice":{"type":"string","enum":options}}});
let fields: Vec<_> = (0..8)
.map(|i| {
field(
i.to_string(),
&json!({"$ref":"#/$defs/choice"}),
&root,
None,
false,
)
})
.collect();
let (page, next) = settings_page(fields.clone()).unwrap();
assert!(page.len() < 8);
assert!(serde_json::to_vec(&page).unwrap().len() < 512 * 1024);
assert_eq!(next, page.last().map(|field| field.key.clone()));
let remainder = fields.into_iter().skip(page.len()).collect();
let (second, _) = settings_page(remainder).unwrap();
assert_eq!(second[0].key, page.len().to_string());
}
}