Skip to main content

agent_first_psql/
config.rs

1use 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
214/// Apply one patch field in place: absent leaves the value, null clears it.
215fn 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;