agent_first_psql/
config.rs1use crate::types::*;
2use agent_first_data::cli_parse_log_filters;
3
4pub const VERSION: &str = env!("CARGO_PKG_VERSION");
5
6impl RuntimeConfig {
7 #[cfg(test)]
8 pub fn resolve_options(&self, q: &QueryOptions) -> ResolvedOptions {
9 self.resolve_options_with_permission(q, q.permission.unwrap_or(Permission::Read))
10 }
11
12 pub fn apply_update(&mut self, patch: ConfigPatch) {
13 if let Some(v) = patch.default_session {
14 self.default_session = v;
15 }
16 if let Some(v) = patch.inline_max_rows {
17 self.inline_max_rows = v;
18 }
19 if let Some(v) = patch.inline_max_bytes {
20 self.inline_max_bytes = v;
21 }
22 if let Some(v) = patch.statement_timeout_ms {
23 self.statement_timeout_ms = v;
24 }
25 if let Some(v) = patch.lock_timeout_ms {
26 self.lock_timeout_ms = v;
27 }
28 if let Some(v) = patch.log {
29 self.log = cli_parse_log_filters(&v);
30 }
31 if let Some(sessions) = patch.sessions {
32 for (name, s) in sessions {
33 let entry = self.sessions.entry(name).or_default();
34 if let Some(v) = s.dsn_secret.into_update() {
35 entry.dsn_secret = v;
36 }
37 if let Some(v) = s.conninfo_secret.into_update() {
38 entry.conninfo_secret = v;
39 }
40 if let Some(v) = s.host.into_update() {
41 entry.host = v;
42 }
43 if let Some(v) = s.port.into_update() {
44 entry.port = v;
45 }
46 if let Some(v) = s.user.into_update() {
47 entry.user = v;
48 }
49 if let Some(v) = s.dbname.into_update() {
50 entry.dbname = v;
51 }
52 if let Some(v) = s.password_secret.into_update() {
53 entry.password_secret = v;
54 }
55 if let Some(v) = s.ssh.destination.into_update() {
56 entry.ssh.destination = v;
57 }
58 if let Some(v) = s.ssh.via.into_update() {
59 entry.ssh.via = v.unwrap_or_default();
60 }
61 if let Some(v) = s.ssh.options.into_update() {
62 entry.ssh.options = v.unwrap_or_default();
63 }
64 if let Some(v) = s.ssh.local_host.into_update() {
65 entry.ssh.local_host = v;
66 }
67 if let Some(v) = s.ssh.local_port.into_update() {
68 entry.ssh.local_port = v;
69 }
70 if let Some(v) = s.ssh.remote_socket.into_update() {
71 entry.ssh.remote_socket = v;
72 }
73 if let Some(v) = s.ssh.sudo_user.into_update() {
74 entry.ssh.sudo_user = v;
75 }
76 let container = s.container;
77 apply_patch(&mut entry.container.docker_name, container.docker_name);
78 apply_patch(&mut entry.container.docker_user, container.docker_user);
79 apply_patch(
80 &mut entry.container.docker_context,
81 container.docker_context,
82 );
83 apply_patch(
84 &mut entry.container.docker_runtime,
85 container.docker_runtime,
86 );
87 apply_patch(&mut entry.container.podman_name, container.podman_name);
88 apply_patch(&mut entry.container.podman_user, container.podman_user);
89 apply_patch(
90 &mut entry.container.podman_runtime,
91 container.podman_runtime,
92 );
93 apply_patch(&mut entry.container.nerdctl_name, container.nerdctl_name);
94 apply_patch(&mut entry.container.nerdctl_user, container.nerdctl_user);
95 apply_patch(
96 &mut entry.container.nerdctl_runtime,
97 container.nerdctl_runtime,
98 );
99 apply_patch(
100 &mut entry.container.compose_service,
101 container.compose_service,
102 );
103 apply_patch(&mut entry.container.compose_user, container.compose_user);
104 if let Some(v) = container.compose_files.into_update() {
105 entry.container.compose_files = v.unwrap_or_default();
106 }
107 apply_patch(
108 &mut entry.container.compose_project,
109 container.compose_project,
110 );
111 apply_patch(
112 &mut entry.container.compose_runtime,
113 container.compose_runtime,
114 );
115 apply_patch(&mut entry.container.kubectl_pod, container.kubectl_pod);
116 apply_patch(
117 &mut entry.container.kubectl_container,
118 container.kubectl_container,
119 );
120 apply_patch(
121 &mut entry.container.kubectl_namespace,
122 container.kubectl_namespace,
123 );
124 apply_patch(
125 &mut entry.container.kubectl_context,
126 container.kubectl_context,
127 );
128 apply_patch(
129 &mut entry.container.kubectl_runtime,
130 container.kubectl_runtime,
131 );
132 }
133 }
134 if !self.sessions.contains_key(&self.default_session) {
135 self.sessions
136 .insert(self.default_session.clone(), SessionConfig::default());
137 }
138 }
139
140 pub fn resolve_options_for_session(
141 &self,
142 q: &QueryOptions,
143 session: &SessionConfig,
144 ) -> Result<ResolvedOptions, String> {
145 let transport = session.transport_kind()?;
146 let permission = q.permission.unwrap_or(match transport {
147 TransportKind::Direct => Permission::Read,
148 TransportKind::Ssh => Permission::SshRead,
149 TransportKind::Container => Permission::ContainerRead,
150 });
151 match transport {
152 TransportKind::Direct if permission.allows_ssh() => {
153 return Err(format!(
154 "permission `{}` requires SSH transport; use `read` or `write` for direct connections",
155 permission.as_str()
156 ));
157 }
158 TransportKind::Direct if permission.allows_container() => {
159 return Err(format!(
160 "permission `{}` requires container transport; use `read` or `write` for direct connections",
161 permission.as_str()
162 ));
163 }
164 TransportKind::Ssh if !permission.allows_ssh() => {
165 return Err(format!(
166 "permission `{}` does not allow SSH transport; use `ssh-read` or `ssh-write`",
167 permission.as_str()
168 ));
169 }
170 TransportKind::Container if !permission.allows_container() => {
171 return Err(format!(
172 "permission `{}` does not allow container transport; use `container-read` or `container-write`",
173 permission.as_str()
174 ));
175 }
176 _ => {}
177 }
178 Ok(self.resolve_options_with_permission(q, permission))
179 }
180
181 pub fn resolve_write_options_for_session(
182 &self,
183 q: &QueryOptions,
184 session: &SessionConfig,
185 ) -> Result<ResolvedOptions, String> {
186 let resolved = self.resolve_options_for_session(q, session)?;
187 if resolved.read_only {
188 return Err(
189 "read-write transaction requires an explicit write permission matching the session transport"
190 .to_string(),
191 );
192 }
193 Ok(resolved)
194 }
195
196 fn resolve_options_with_permission(
197 &self,
198 q: &QueryOptions,
199 permission: Permission,
200 ) -> ResolvedOptions {
201 ResolvedOptions {
202 stream_rows: q.stream_rows,
203 batch_rows: q.batch_rows.unwrap_or(1000).max(1),
204 batch_bytes: q.batch_bytes.unwrap_or(262_144).max(1024),
205 statement_timeout_ms: q.statement_timeout_ms.unwrap_or(self.statement_timeout_ms),
206 lock_timeout_ms: q.lock_timeout_ms.unwrap_or(self.lock_timeout_ms),
207 read_only: permission.is_read_only(),
208 inline_max_rows: q.inline_max_rows.unwrap_or(self.inline_max_rows),
209 inline_max_bytes: q.inline_max_bytes.unwrap_or(self.inline_max_bytes),
210 }
211 }
212}
213
214fn apply_patch<T>(field: &mut Option<T>, patch: PatchField<T>) {
216 if let Some(update) = patch.into_update() {
217 *field = update;
218 }
219}
220
221pub fn sessions_to_invalidate(patch: &ConfigPatch) -> Vec<String> {
222 let mut sessions: Vec<String> = vec![];
223 if let Some(default_session) = patch.default_session.as_ref() {
224 sessions.push(default_session.clone());
225 }
226 if let Some(update_sessions) = patch.sessions.as_ref() {
227 sessions.extend(update_sessions.keys().cloned());
228 }
229 sessions.sort();
230 sessions.dedup();
231 sessions
232}
233
234#[cfg(test)]
235#[path = "../tests/support/unit_config.rs"]
236mod tests;