Skip to content

Commit a27dcd0

Browse files
committed
agent-config: add support for agent config to cli/manifest and make naming in api more consistent
1 parent 457c1f3 commit a27dcd0

File tree

61 files changed

+1073
-597
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1073
-597
lines changed

cli/golem-cli/src/command.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use golem_common::model::agent::AgentTypeName;
4747
use golem_common::model::application::ApplicationName;
4848
use golem_common::model::component::{ComponentName, ComponentRevision};
4949
use golem_common::model::deployment::DeploymentRevision;
50+
use golem_common::model::worker::WorkerAgentConfigEntry;
5051
use lenient_bool::LenientBool;
5152
use std::collections::{BTreeSet, HashMap};
5253
use std::ffi::OsString;
@@ -1072,6 +1073,7 @@ pub mod component {
10721073
pub mod worker {
10731074
use crate::command::parse_cursor;
10741075
use crate::command::parse_key_val;
1076+
use crate::command::parse_worker_agent_config;
10751077
use crate::command::shared_args::{
10761078
AgentIdArgs, PostDeployArgs, StreamArgs, WorkerFunctionArgument, WorkerFunctionName,
10771079
};
@@ -1080,6 +1082,7 @@ pub mod worker {
10801082
use clap::Subcommand;
10811083
use golem_client::model::ScanCursor;
10821084
use golem_common::model::component::{ComponentName, ComponentRevision};
1085+
use golem_common::model::worker::WorkerAgentConfigEntry;
10831086
use golem_common::model::IdempotencyKey;
10841087
use uuid::Uuid;
10851088

@@ -1095,6 +1098,9 @@ pub mod worker {
10951098
/// wasi:config entries visible for the agent
10961099
#[arg(short, long, value_parser = parse_key_val, value_name = "VAR=VAL")]
10971100
config_vars: Vec<(String, String)>,
1101+
/// agent config for entries
1102+
#[arg(short, long, value_parser = parse_worker_agent_config)]
1103+
agent_config: Vec<WorkerAgentConfigEntry>,
10981104
},
10991105
// TODO: json args
11001106
/// Invoke (or enqueue invocation for) agent
@@ -1654,6 +1660,91 @@ fn parse_key_val(key_and_val: &str) -> anyhow::Result<(String, String)> {
16541660
))
16551661
}
16561662

1663+
fn parse_worker_agent_config(s: &str) -> anyhow::Result<WorkerAgentConfigEntry> {
1664+
let (path, value) = split_worker_agent_config_path_and_value(s)?;
1665+
1666+
let path = parse_worker_agent_config_path(path)?;
1667+
1668+
let value: serde_json::Value = serde_json::from_str(value)?;
1669+
1670+
Ok(WorkerAgentConfigEntry { path, value })
1671+
}
1672+
1673+
fn split_worker_agent_config_path_and_value(input: &str) -> anyhow::Result<(&str, &str)> {
1674+
let chars = input.char_indices();
1675+
let mut in_quotes = false;
1676+
let mut escape = false;
1677+
1678+
for (i, c) in chars {
1679+
if escape {
1680+
escape = false;
1681+
continue;
1682+
}
1683+
1684+
match c {
1685+
'\\' => escape = true,
1686+
'"' => in_quotes = !in_quotes,
1687+
'=' if !in_quotes => {
1688+
let key = &input[..i];
1689+
let value = &input[i + 1..];
1690+
return Ok((key, value));
1691+
}
1692+
_ => {}
1693+
}
1694+
}
1695+
1696+
Err(anyhow!("expected unescaped '=' separating key and value"))
1697+
}
1698+
1699+
fn parse_worker_agent_config_path(input: &str) -> anyhow::Result<Vec<String>> {
1700+
let mut keys = Vec::new();
1701+
let mut buf = String::new();
1702+
1703+
let mut chars = input.chars().peekable();
1704+
let mut in_quotes = false;
1705+
1706+
while let Some(c) = chars.next() {
1707+
match c {
1708+
'\\' => {
1709+
// escape next char
1710+
let next = chars.next().ok_or_else(|| anyhow!("dangling escape"))?;
1711+
buf.push(next);
1712+
}
1713+
1714+
'"' => {
1715+
in_quotes = !in_quotes;
1716+
}
1717+
1718+
'.' if !in_quotes => {
1719+
push_agent_config_path_segment(&mut keys, &mut buf)?;
1720+
}
1721+
1722+
_ => buf.push(c),
1723+
}
1724+
}
1725+
1726+
if in_quotes {
1727+
return Err(anyhow!("unterminated quote"));
1728+
}
1729+
1730+
push_agent_config_path_segment(&mut keys, &mut buf)?;
1731+
1732+
Ok(keys)
1733+
}
1734+
1735+
fn push_agent_config_path_segment(keys: &mut Vec<String>, buf: &mut String) -> anyhow::Result<()> {
1736+
let segment = buf.trim();
1737+
1738+
if segment.is_empty() {
1739+
return Err(anyhow!("empty path segment"));
1740+
}
1741+
1742+
keys.push(segment.to_string());
1743+
buf.clear();
1744+
1745+
Ok(())
1746+
}
1747+
16571748
// TODO: better error context and messages
16581749
fn parse_cursor(cursor: &str) -> anyhow::Result<ScanCursor> {
16591750
let parts = cursor.split('/').collect::<Vec<_>>();
@@ -1921,3 +2012,113 @@ mod test {
19212012
}
19222013
}
19232014
}
2015+
2016+
#[cfg(test)]
2017+
mod parse_worker_agent_config_tests {
2018+
use crate::command::{parse_worker_agent_config, parse_worker_agent_config_path};
2019+
use golem_common::model::worker::WorkerAgentConfigEntry;
2020+
use serde_json::json;
2021+
use test_r::test;
2022+
2023+
fn parse(input: &str) -> WorkerAgentConfigEntry {
2024+
parse_worker_agent_config(input).unwrap()
2025+
}
2026+
2027+
#[test]
2028+
fn simple_path() {
2029+
let e = parse(r#"a.b.c=1"#);
2030+
2031+
assert_eq!(e.path, vec!["a", "b", "c"]);
2032+
assert_eq!(e.value, json!(1));
2033+
}
2034+
2035+
#[test]
2036+
fn string_value() {
2037+
let e = parse(r#"a.b="hello""#);
2038+
2039+
assert_eq!(e.path, vec!["a", "b"]);
2040+
assert_eq!(e.value, json!("hello"));
2041+
}
2042+
2043+
#[test]
2044+
fn json_object_value() {
2045+
let e = parse(r#"a.b={"x":1,"y":2}"#);
2046+
2047+
assert_eq!(e.path, vec!["a", "b"]);
2048+
assert_eq!(e.value, json!({"x":1,"y":2}));
2049+
}
2050+
2051+
#[test]
2052+
fn quoted_path_segment() {
2053+
let e = parse(r#""foo.bar".baz=1"#);
2054+
2055+
assert_eq!(e.path, vec!["foo.bar", "baz"]);
2056+
assert_eq!(e.value, json!(1));
2057+
}
2058+
2059+
#[test]
2060+
fn quoted_segment_with_spaces() {
2061+
let e = parse(r#""foo bar".baz=1"#);
2062+
2063+
assert_eq!(e.path, vec!["foo bar", "baz"]);
2064+
assert_eq!(e.value, json!(1));
2065+
}
2066+
2067+
#[test]
2068+
fn escaped_dot_in_segment() {
2069+
let e = parse(r#"foo\.bar.baz=1"#);
2070+
2071+
assert_eq!(e.path, vec!["foo.bar", "baz"]);
2072+
assert_eq!(e.value, json!(1));
2073+
}
2074+
2075+
#[test]
2076+
fn equals_inside_value() {
2077+
let e = parse(r#"a.b="foo=bar""#);
2078+
2079+
assert_eq!(e.path, vec!["a", "b"]);
2080+
assert_eq!(e.value, json!("foo=bar"));
2081+
}
2082+
2083+
#[test]
2084+
fn equals_inside_path() {
2085+
let e = parse(r#""foo=bar".baz=1"#);
2086+
2087+
assert_eq!(e.path, vec!["foo=bar", "baz"]);
2088+
assert_eq!(e.value, json!(1));
2089+
}
2090+
2091+
#[test]
2092+
fn escaped_equals_in_path() {
2093+
let e = parse(r#"foo\=bar.baz=1"#);
2094+
2095+
assert_eq!(e.path, vec!["foo=bar", "baz"]);
2096+
assert_eq!(e.value, json!(1));
2097+
}
2098+
2099+
#[test]
2100+
fn complex_case() {
2101+
let e = parse(r#""foo.bar=baz"."x.y"={"hello":"world"}"#);
2102+
2103+
assert_eq!(e.path, vec!["foo.bar=baz", "x.y"]);
2104+
assert_eq!(e.value, json!({"hello":"world"}));
2105+
}
2106+
2107+
#[test]
2108+
fn split_fails_without_equals() {
2109+
let err = parse_worker_agent_config("a.b.c").unwrap_err();
2110+
assert!(err.to_string().contains("expected unescaped '='"));
2111+
}
2112+
2113+
#[test]
2114+
fn unterminated_quote_in_path() {
2115+
let err = parse_worker_agent_config_path(r#""foo.bar.baz"#).unwrap_err();
2116+
assert!(err.to_string().contains("unterminated quote"));
2117+
}
2118+
2119+
#[test]
2120+
fn dangling_escape() {
2121+
let err = parse_worker_agent_config_path(r#"foo.bar\"#).unwrap_err();
2122+
assert!(err.to_string().contains("dangling escape"));
2123+
}
2124+
}

cli/golem-cli/src/command_handler/app/deploy_diff.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -776,9 +776,7 @@ fn normalized_diff_deployment(
776776
wasm_hash: component.wasm_hash,
777777
files_by_path: component.files_by_path.clone(),
778778
plugins_by_grant_id: component.plugins_by_grant_id.clone(),
779-
local_agent_config_ordered_by_agent_and_key: component
780-
.local_agent_config_ordered_by_agent_and_key
781-
.clone(),
779+
ordered_agent_config: component.ordered_agent_config.clone(),
782780
}
783781
.into(),
784782
None => component.hash().into(),

cli/golem-cli/src/command_handler/app/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,14 @@ impl AppCommandHandler {
15211521
&self,
15221522
deploy_diff: &DeployDiff,
15231523
) -> anyhow::Result<CurrentDeployment> {
1524+
let agent_secret_defaults = {
1525+
let app_ctx = self.ctx.app_context_lock().await;
1526+
app_ctx
1527+
.some_or_err()?
1528+
.application()
1529+
.deployment_agent_secret_defaults(&deploy_diff.environment.environment_name)
1530+
};
1531+
15241532
let clients = self.ctx.golem_clients().await?;
15251533

15261534
log_action("Deploying", "staged changes to the environment");
@@ -1533,8 +1541,7 @@ impl AppCommandHandler {
15331541
current_revision: deploy_diff.current_deployment_revision(),
15341542
expected_deployment_hash: deploy_diff.local_deployment_hash,
15351543
version: DeploymentVersion("".to_string()), // TODO: atomic
1536-
// FIXME: agent-config
1537-
agent_secret_defaults: Vec::new(),
1544+
agent_secret_defaults,
15381545
},
15391546
)
15401547
.await

cli/golem-cli/src/command_handler/component/mod.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use golem_common::model::component::{
5252
ComponentId, ComponentName, ComponentRevision, ComponentUpdate,
5353
};
5454
use golem_common::model::deployment::DeploymentPlanComponentEntry;
55-
use golem_common::model::diff;
55+
use golem_common::model::diff::{self, VecDiffable};
5656
use golem_common::model::environment::EnvironmentName;
5757
use itertools::Itertools;
5858
use std::collections::{BTreeMap, HashMap, HashSet};
@@ -883,6 +883,7 @@ impl ComponentCommandHandler {
883883
let plugins = component.plugins().clone();
884884
let env = resolve_env_vars(component_name, component.env())?;
885885
let config_vars = component.config_vars().clone();
886+
let agent_config = component.agent_config().clone();
886887

887888
Ok(ComponentDeployProperties {
888889
wasm_path,
@@ -891,6 +892,7 @@ impl ComponentCommandHandler {
891892
plugins,
892893
env,
893894
config_vars,
895+
agent_config,
894896
})
895897
}
896898

@@ -997,8 +999,16 @@ impl ComponentCommandHandler {
997999
wasm_hash: component_binary_hash.into(),
9981000
files_by_path,
9991001
plugins_by_grant_id,
1000-
// FIXME: agent-config
1001-
local_agent_config_ordered_by_agent_and_key: Vec::new(),
1002+
ordered_agent_config: properties
1003+
.agent_config
1004+
.iter()
1005+
.map(|lac| diff::AgentConfigEntry {
1006+
agent: lac.agent.0.clone(),
1007+
path: lac.path.clone(),
1008+
value: diff::into_normalized_json(lac.value.clone()),
1009+
})
1010+
.sorted_by(|v1, v2| v1.ordering_key().cmp(&v2.ordering_key()))
1011+
.collect(),
10021012
})
10031013
}
10041014

@@ -1045,8 +1055,7 @@ impl ComponentCommandHandler {
10451055
.unwrap_or_default(),
10461056
env: component_stager.env(),
10471057
config_vars: component_stager.config_vars(),
1048-
// FIXME: agent-config
1049-
local_agent_config: Vec::new(),
1058+
agent_config: component_stager.agent_config(),
10501059
agent_types,
10511060
plugins: component_stager.plugins(),
10521061
},
@@ -1141,8 +1150,7 @@ impl ComponentCommandHandler {
11411150
removed_files: changed_files.removed.clone(),
11421151
new_file_options: changed_files.merged_file_options(),
11431152
config_vars: component_stager.config_vars_if_changed(),
1144-
// FIXME: agent-config
1145-
local_agent_config: None,
1153+
agent_config: component_stager.agent_config_if_changed(),
11461154
env: component_stager.env_if_changed(),
11471155
agent_types,
11481156
plugin_updates: component_stager.plugins_if_changed(),

cli/golem-cli/src/command_handler/component/staging.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use anyhow::{anyhow, Context as AnyhowContext};
2222
use golem_client::model::EnvironmentPluginGrantWithDetails;
2323
use golem_common::model::agent::AgentType;
2424
use golem_common::model::component::{
25-
ComponentFileOptions, ComponentFilePath, PluginInstallation, PluginInstallationAction,
26-
PluginInstallationUpdate, PluginPriority, PluginUninstallation,
25+
AgentConfigEntry, ComponentFileOptions, ComponentFilePath, PluginInstallation,
26+
PluginInstallationAction, PluginInstallationUpdate, PluginPriority, PluginUninstallation,
2727
};
2828

2929
use golem_common::model::diff;
@@ -62,6 +62,13 @@ impl ComponentDiff {
6262
ComponentDiff::Diff { diff } => diff.metadata_changed,
6363
}
6464
}
65+
66+
pub fn agent_config_changed(&self) -> bool {
67+
match self {
68+
ComponentDiff::All => true,
69+
ComponentDiff::Diff { diff } => !diff.agent_config_changes.is_empty(),
70+
}
71+
}
6572
}
6673

6774
pub struct ChangedComponentFiles {
@@ -269,6 +276,18 @@ impl<'a> ComponentStager<'a> {
269276
}
270277
}
271278

279+
pub fn agent_config(&self) -> Vec<AgentConfigEntry> {
280+
self.component_deploy_properties.agent_config.clone()
281+
}
282+
283+
pub fn agent_config_if_changed(&self) -> Option<Vec<AgentConfigEntry>> {
284+
if self.diff.agent_config_changed() {
285+
Some(self.agent_config())
286+
} else {
287+
None
288+
}
289+
}
290+
272291
pub fn plugins(&self) -> Vec<PluginInstallation> {
273292
self.component_deploy_properties
274293
.plugins

0 commit comments

Comments
 (0)