gaia_client/client.rs
1use crate::config::GaiaClientConfig;
2use crate::error::Result;
3use crate::proto::gaia_client_client::GaiaClientClient;
4use crate::proto::{
5 GetSecretRequest, Namespace, Secret,
6};
7use crate::tls::create_tls_channel;
8use tonic::transport::Channel;
9
10/// Options for loading secrets into the environment.
11#[derive(Debug, Clone, Default)]
12pub struct LoadEnvOptions {
13 /// Prefix to prepend to environment variable names.
14 /// If strictly empty or None, no prefix is added.
15 pub prefix: Option<String>,
16
17 /// Whether to include the namespace in the environment variable name.
18 pub use_namespace: bool,
19}
20
21impl LoadEnvOptions {
22 /// Create new options with default values (key only).
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 /// Set the prefix.
28 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
29 self.prefix = Some(prefix.into());
30 self
31 }
32
33 /// Set whether to use the namespace.
34 pub fn with_namespace(mut self, use_namespace: bool) -> Self {
35 self.use_namespace = use_namespace;
36 self
37 }
38}
39
40/// A client for interacting with the Gaia secret management daemon.
41///
42/// The client uses mutual TLS (mTLS) for secure communication and provides
43/// methods to retrieve secrets and check daemon status.
44pub struct GaiaClient {
45 inner: GaiaClientClient<Channel>,
46}
47
48impl GaiaClient {
49 /// Connects to the Gaia daemon using the provided configuration.
50 ///
51 /// # Arguments
52 ///
53 /// * `config` - Configuration containing server address and TLS certificates
54 ///
55 /// # Errors
56 ///
57 /// Returns an error if:
58 /// - TLS certificates cannot be loaded
59 /// - Connection to the daemon fails
60 ///
61 /// # Example
62 ///
63 /// ```no_run
64 /// use gaia_client::{GaiaClient, GaiaClientConfig};
65 ///
66 /// #[tokio::main]
67 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
68 /// let config = GaiaClientConfig::new(
69 /// "localhost:50051",
70 /// "/etc/gaia/certs/ca.crt",
71 /// "/etc/gaia/certs/client.crt",
72 /// "/etc/gaia/certs/client.key",
73 /// );
74 ///
75 /// let client = GaiaClient::connect(config).await?;
76 /// Ok(())
77 /// }
78 /// ```
79 pub async fn connect(config: GaiaClientConfig) -> Result<Self> {
80 let channel = create_tls_channel(&config).await?;
81 let inner = GaiaClientClient::new(channel);
82 Ok(Self { inner })
83 }
84
85 /// Retrieves a secret from the specified namespace.
86 ///
87 /// # Arguments
88 ///
89 /// * `namespace` - The namespace containing the secret
90 /// * `id` - The secret identifier
91 ///
92 /// # Errors
93 ///
94 /// Returns an error if:
95 /// - The daemon is locked
96 /// - The secret does not exist
97 /// - A network error occurs
98 ///
99 /// # Example
100 ///
101 /// ```no_run
102 /// # use gaia_client::{GaiaClient, GaiaClientConfig};
103 /// # #[tokio::main]
104 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105 /// # let config = GaiaClientConfig::new("localhost:50051", "ca.crt", "client.crt", "client.key");
106 /// # let mut client = GaiaClient::connect(config).await?;
107 /// let secret = client.get_secret("production", "database_url").await?;
108 /// println!("Secret: {}", secret.value);
109 /// # Ok(())
110 /// # }
111 /// ```
112 pub async fn get_secret(&mut self, namespace: &str, id: &str) -> Result<Secret> {
113 let request = tonic::Request::new(GetSecretRequest {
114 namespace: namespace.to_string(),
115 id: id.to_string(),
116 });
117
118 let response = self.inner.get_secret(request).await?;
119 Ok(response.into_inner())
120 }
121
122
123
124
125
126
127
128
129
130 /// Lists all secrets for the authenticated client.
131 ///
132 /// Returns secrets from the client's own namespaces plus the common namespace.
133 /// If a namespace filter is provided, only secrets from that namespace are returned.
134 ///
135 /// # Arguments
136 ///
137 /// * `namespace` - Optional namespace filter
138 ///
139 /// # Example
140 ///
141 /// ```no_run
142 /// # use gaia_client::{GaiaClient, GaiaClientConfig};
143 /// # #[tokio::main]
144 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
145 /// # let config = GaiaClientConfig::new("localhost:50051", "ca.crt", "client.crt", "client.key");
146 /// # let mut client = GaiaClient::connect(config).await?;
147 /// // Get all secrets (client's own + common)
148 /// let all_secrets = client.list_secrets(None).await?;
149 ///
150 /// // Get only secrets from a specific namespace
151 /// let prod_secrets = client.list_secrets(Some("production".to_string())).await?;
152 ///
153 /// for namespace in all_secrets {
154 /// println!("Namespace: {}", namespace.name);
155 /// for secret in namespace.secrets {
156 /// println!(" - {}: {}", secret.id, secret.value);
157 /// }
158 /// }
159 /// # Ok(())
160 /// # }
161 /// ```
162 pub async fn list_secrets(&mut self, namespace: Option<String>) -> Result<Vec<Namespace>> {
163 use crate::proto::ClientListSecretsRequest;
164
165 let request = tonic::Request::new(ClientListSecretsRequest {
166 namespace: namespace.unwrap_or_default(),
167 });
168
169 let response = self.inner.list_secrets(request).await?;
170 Ok(response.into_inner().namespaces)
171 }
172
173 /// Fetches all accessible secrets and loads them into the current process's environment.
174 ///
175 /// By default, environment variables are named after the secret key, converted to uppercase
176 /// with hyphens replaced by underscores. Optional prefix and namespace inclusion can be
177 /// configured via `LoadEnvOptions`.
178 ///
179 /// # Arguments
180 ///
181 /// * `options` - Optional configuration for env var naming
182 ///
183 /// # Example
184 ///
185 /// ```no_run
186 /// # use gaia_client::{GaiaClient, GaiaClientConfig, LoadEnvOptions};
187 /// # #[tokio::main]
188 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
189 /// # let config = GaiaClientConfig::new("localhost:50051", "ca.crt", "client.crt", "client.key");
190 /// # let mut client = GaiaClient::connect(config).await?;
191 /// // Load with default behavior (key only)
192 /// client.load_env(None).await?;
193 ///
194 /// // Load with prefix and namespace: GAIA_PRODUCTION_DATABASE_URL
195 /// client.load_env(Some(LoadEnvOptions::new().with_prefix("GAIA").with_namespace(true))).await?;
196 /// # Ok(())
197 /// # }
198 /// ```
199 pub async fn load_env(&mut self, options: Option<LoadEnvOptions>) -> Result<()> {
200 let opts = options.unwrap_or_default();
201 let namespaces = self.list_secrets(None).await?;
202
203 for ns in namespaces {
204 for secret in ns.secrets {
205 let mut parts = Vec::new();
206
207 if let Some(prefix) = &opts.prefix {
208 if !prefix.is_empty() {
209 parts.push(prefix.to_uppercase().replace("-", "_"));
210 }
211 }
212
213 if opts.use_namespace {
214 parts.push(ns.name.to_uppercase().replace("-", "_"));
215 }
216
217 parts.push(secret.id.to_uppercase().replace("-", "_"));
218
219 let env_var_name = parts.join("_");
220 std::env::set_var(env_var_name, secret.value);
221 }
222 }
223
224 Ok(())
225 }
226}