Skip to main content

cli/common/
store.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17use clap::{Parser, ValueEnum};
18use common_error::ext::BoxedError;
19use common_meta::kv_backend::KvBackendRef;
20use common_meta::kv_backend::chroot::ChrootKvBackend;
21use common_meta::kv_backend::etcd::EtcdStore;
22use meta_srv::metasrv::BackendClientOptions;
23use meta_srv::utils::etcd::create_etcd_client_with_tls;
24use serde::{Deserialize, Serialize};
25use servers::tls::{TlsMode, TlsOption};
26use snafu::OptionExt;
27
28use crate::error::{EmptyStoreAddrsSnafu, InvalidArgumentsSnafu};
29
30// The datastores that implements metadata kvbackend.
31#[derive(Clone, Debug, PartialEq, Serialize, Default, Deserialize, ValueEnum)]
32#[serde(rename_all = "snake_case")]
33#[allow(clippy::enum_variant_names)]
34pub enum BackendImpl {
35    // Etcd as metadata storage.
36    #[default]
37    EtcdStore,
38    // In memory metadata storage - mostly used for testing.
39    MemoryStore,
40    #[cfg(feature = "pg_kvbackend")]
41    // Postgres as metadata storage.
42    PostgresStore,
43    #[cfg(feature = "mysql_kvbackend")]
44    // MySql as metadata storage.
45    MysqlStore,
46    // RaftEngine as metadata storage.
47    RaftEngineStore,
48}
49
50#[derive(Debug, Default, Parser)]
51pub struct StoreConfig {
52    /// The endpoint of store. one of etcd, postgres or mysql.
53    ///
54    /// For postgres store, the format is:
55    /// "password=password dbname=postgres user=postgres host=localhost port=5432"
56    ///
57    /// For etcd store, the format is:
58    /// "127.0.0.1:2379"
59    ///
60    /// For mysql store, the format is:
61    /// "mysql://user:password@ip:port/dbname"
62    #[clap(long, alias = "store-addr", value_delimiter = ',', num_args = 1..)]
63    pub store_addrs: Vec<String>,
64
65    /// The maximum number of operations in a transaction. Only used when using [etcd-store].
66    #[clap(long, default_value = "128")]
67    pub max_txn_ops: usize,
68
69    /// The metadata store backend.
70    #[clap(long, value_enum, default_value = "etcd-store")]
71    pub backend: BackendImpl,
72
73    /// The key prefix of the metadata store.
74    #[clap(long, default_value = "")]
75    pub store_key_prefix: String,
76
77    /// The table name in RDS to store metadata. Only used when using [postgres-store] or [mysql-store].
78    #[cfg(any(feature = "pg_kvbackend", feature = "mysql_kvbackend"))]
79    #[clap(long, default_value = common_meta::kv_backend::DEFAULT_META_TABLE_NAME)]
80    pub meta_table_name: String,
81
82    /// Optional PostgreSQL schema for metadata table (defaults to current search_path if unset).
83    #[cfg(feature = "pg_kvbackend")]
84    #[clap(long)]
85    pub meta_schema_name: Option<String>,
86
87    /// Automatically create PostgreSQL schema if it doesn't exist (default: true).
88    #[cfg(feature = "pg_kvbackend")]
89    #[clap(
90        long,
91        default_value_t = true,
92        default_missing_value = "true",
93        num_args = 0..=1,
94        action = clap::ArgAction::Set
95    )]
96    pub auto_create_schema: bool,
97
98    /// TLS mode for backend store connections (etcd, PostgreSQL, MySQL)
99    #[clap(long = "backend-tls-mode", value_enum, default_value = "disable")]
100    pub backend_tls_mode: TlsMode,
101
102    /// Path to TLS certificate file for backend store connections
103    #[clap(long = "backend-tls-cert-path", default_value = "")]
104    pub backend_tls_cert_path: String,
105
106    /// Path to TLS private key file for backend store connections
107    #[clap(long = "backend-tls-key-path", default_value = "")]
108    pub backend_tls_key_path: String,
109
110    /// Path to TLS CA certificate file for backend store connections
111    #[clap(long = "backend-tls-ca-cert-path", default_value = "")]
112    pub backend_tls_ca_cert_path: String,
113
114    /// Enable watching TLS certificate files for changes
115    #[clap(long = "backend-tls-watch")]
116    pub backend_tls_watch: bool,
117}
118
119impl StoreConfig {
120    pub fn tls_config(&self) -> Option<TlsOption> {
121        if self.backend_tls_mode != TlsMode::Disable {
122            Some(TlsOption {
123                mode: self.backend_tls_mode,
124                cert_path: self.backend_tls_cert_path.clone(),
125                key_path: self.backend_tls_key_path.clone(),
126                ca_cert_path: self.backend_tls_ca_cert_path.clone(),
127                watch: self.backend_tls_watch,
128            })
129        } else {
130            None
131        }
132    }
133
134    /// Sanitize store addrs for logging (redacts passwords in connection strings).
135    fn sanitize_store_addrs(&self) -> Vec<String> {
136        self.store_addrs
137            .iter()
138            .map(|addr| common_meta::kv_backend::util::sanitize_connection_string(addr))
139            .collect()
140    }
141
142    /// Builds a [`KvBackendRef`] from the store configuration.
143    pub async fn build(&self) -> Result<KvBackendRef, BoxedError> {
144        let max_txn_ops = self.max_txn_ops;
145        let store_addrs = &self.store_addrs;
146        if store_addrs.is_empty() {
147            EmptyStoreAddrsSnafu.fail().map_err(BoxedError::new)
148        } else {
149            common_telemetry::info!(
150                "Building kvbackend with store addrs: {:?}, backend: {:?}",
151                &self.sanitize_store_addrs(),
152                self.backend
153            );
154            let kvbackend = match self.backend {
155                BackendImpl::EtcdStore => {
156                    let tls_config = self.tls_config();
157                    let etcd_client = create_etcd_client_with_tls(
158                        store_addrs,
159                        &BackendClientOptions::default(),
160                        tls_config.as_ref(),
161                    )
162                    .await
163                    .map_err(BoxedError::new)?;
164                    Ok(EtcdStore::with_etcd_client(etcd_client, max_txn_ops))
165                }
166                #[cfg(feature = "pg_kvbackend")]
167                BackendImpl::PostgresStore => {
168                    let table_name = &self.meta_table_name;
169                    let tls_config = self.tls_config();
170                    Ok(meta_srv::utils::postgres::build_postgres_kv_backend(
171                        store_addrs,
172                        None,
173                        tls_config,
174                        self.meta_schema_name.as_deref(),
175                        table_name,
176                        max_txn_ops,
177                        self.auto_create_schema,
178                    )
179                    .await
180                    .map_err(BoxedError::new)?)
181                }
182                #[cfg(feature = "mysql_kvbackend")]
183                BackendImpl::MysqlStore => {
184                    let table_name = &self.meta_table_name;
185                    let tls_config = self.tls_config();
186                    Ok(meta_srv::utils::mysql::build_mysql_kv_backend(
187                        store_addrs,
188                        tls_config.as_ref(),
189                        table_name,
190                        max_txn_ops,
191                    )
192                    .await
193                    .map_err(BoxedError::new)?)
194                }
195                #[cfg(not(test))]
196                BackendImpl::MemoryStore => {
197                    use crate::error::UnsupportedMemoryBackendSnafu;
198
199                    UnsupportedMemoryBackendSnafu
200                        .fail()
201                        .map_err(BoxedError::new)
202                }
203                #[cfg(test)]
204                BackendImpl::MemoryStore => {
205                    use common_meta::kv_backend::memory::MemoryKvBackend;
206
207                    Ok(Arc::new(MemoryKvBackend::default()) as _)
208                }
209                BackendImpl::RaftEngineStore => {
210                    let url = store_addrs
211                        .first()
212                        .context(InvalidArgumentsSnafu {
213                            msg: "empty store addresses".to_string(),
214                        })
215                        .map_err(BoxedError::new)?;
216                    let kvbackend =
217                        standalone::build_metadata_kv_from_url(url).map_err(BoxedError::new)?;
218
219                    Ok(kvbackend)
220                }
221            };
222            if self.store_key_prefix.is_empty() {
223                kvbackend
224            } else {
225                let chroot_kvbackend =
226                    ChrootKvBackend::new(self.store_key_prefix.as_bytes().to_vec(), kvbackend?);
227                Ok(Arc::new(chroot_kvbackend))
228            }
229        }
230    }
231}
232
233#[cfg(all(test, feature = "pg_kvbackend"))]
234mod tests {
235    use clap::Parser;
236
237    use super::StoreConfig;
238
239    #[test]
240    fn test_auto_create_schema_accepts_explicit_value() {
241        let config =
242            StoreConfig::try_parse_from(["store", "--auto-create-schema", "false"]).unwrap();
243        assert!(!config.auto_create_schema);
244
245        let config =
246            StoreConfig::try_parse_from(["store", "--auto-create-schema", "true"]).unwrap();
247        assert!(config.auto_create_schema);
248    }
249
250    #[test]
251    fn test_auto_create_schema_defaults_to_true() {
252        let config = StoreConfig::try_parse_from(["store"]).unwrap();
253        assert!(config.auto_create_schema);
254    }
255}