You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2023/07/21 03:25:50 UTC

[incubator-opendal] branch main updated: feat(services/etcd): introduce new service backend etcd (#2672)

This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 5750dfc69 feat(services/etcd): introduce new service backend etcd (#2672)
5750dfc69 is described below

commit 5750dfc694b5f7ad4921921ef8fdffd46e6c5376
Author: G-XD <38...@users.noreply.github.com>
AuthorDate: Fri Jul 21 11:25:45 2023 +0800

    feat(services/etcd): introduce new service backend etcd (#2672)
    
    * feat(services/etcd): introduce new service backend etcd
    
    * feat(services/etcd): add tls support
    
    * style: code format
    
    * refactor: alphabet order
    
    * trigger build
    
    * refactor: code optimization
    
    ---------
    
    Co-authored-by: GXD <ge...@highlight.mobi>
---
 Cargo.lock                         | 163 +++++++++++++++++-
 bin/oli/Cargo.toml                 |   2 +
 bin/oli/src/config/mod.rs          |   5 +
 core/Cargo.toml                    |   2 +
 core/src/services/etcd/backend.rs  | 328 +++++++++++++++++++++++++++++++++++++
 core/src/services/etcd/docs.md     |  46 ++++++
 core/src/services/etcd/mod.rs      |  19 +++
 core/src/services/mod.rs           |   5 +
 core/src/types/operator/builder.rs |   2 +
 core/src/types/scheme.rs           |   4 +
 10 files changed, 575 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock
index 259e164d3..27548bc25 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -517,7 +517,7 @@ dependencies = [
  "lazy_static",
  "lazycell",
  "peeking_take_while",
- "prettyplease",
+ "prettyplease 0.2.9",
  "proc-macro2",
  "quote",
  "regex",
@@ -1398,6 +1398,22 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "etcd-client"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4b0ea5ef6dc2388a4b1669fa32097249bc03a15417b97cb75e38afb309e4a89"
+dependencies = [
+ "http",
+ "prost",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tonic-build",
+ "tower",
+ "tower-service",
+]
+
 [[package]]
 name = "event-listener"
 version = "2.5.3"
@@ -1413,6 +1429,12 @@ dependencies = [
  "instant",
 ]
 
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
 [[package]]
 name = "flagset"
 version = "0.4.3"
@@ -1922,6 +1944,18 @@ dependencies = [
  "tokio-rustls",
 ]
 
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tokio-io-timeout",
+]
+
 [[package]]
 name = "hyper-tls"
 version = "0.5.0"
@@ -2662,6 +2696,12 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
 [[package]]
 name = "naive-timer"
 version = "0.2.0"
@@ -2977,6 +3017,7 @@ dependencies = [
  "dashmap",
  "dirs 5.0.1",
  "dotenvy",
+ "etcd-client",
  "flagset",
  "futures",
  "governor",
@@ -3510,6 +3551,16 @@ dependencies = [
  "zigzag",
 ]
 
+[[package]]
+name = "petgraph"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+dependencies = [
+ "fixedbitset",
+ "indexmap 1.9.3",
+]
+
 [[package]]
 name = "pin-project"
 version = "1.1.2"
@@ -3677,6 +3728,16 @@ dependencies = [
  "yansi",
 ]
 
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "prettyplease"
 version = "0.2.9"
@@ -3766,6 +3827,28 @@ dependencies = [
  "prost-derive",
 ]
 
+[[package]]
+name = "prost-build"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prettyplease 0.1.25",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 1.0.109",
+ "tempfile",
+ "which",
+]
+
 [[package]]
 name = "prost-derive"
 version = "0.11.9"
@@ -3779,6 +3862,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "prost-types"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
+dependencies = [
+ "prost",
+]
+
 [[package]]
 name = "protobuf"
 version = "2.28.0"
@@ -5143,6 +5235,16 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-io-utility"
 version = "0.7.6"
@@ -5262,6 +5364,50 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "tonic"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64 0.21.2",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "rustls-pemfile",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6fdaae4c2c638bb70fe42803a26fbd6fc6ac8c72f5c59f67ecc2a2dcabf4b07"
+dependencies = [
+ "prettyplease 0.1.25",
+ "proc-macro2",
+ "prost-build",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "tower"
 version = "0.4.13"
@@ -5270,9 +5416,13 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
 dependencies = [
  "futures-core",
  "futures-util",
+ "indexmap 1.9.3",
  "pin-project",
  "pin-project-lite",
+ "rand 0.8.5",
+ "slab",
  "tokio",
+ "tokio-util",
  "tower-layer",
  "tower-service",
  "tracing",
@@ -5749,6 +5899,17 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
 [[package]]
 name = "widestring"
 version = "1.0.2"
diff --git a/bin/oli/Cargo.toml b/bin/oli/Cargo.toml
index dbfa7cc05..407f80f9e 100644
--- a/bin/oli/Cargo.toml
+++ b/bin/oli/Cargo.toml
@@ -32,6 +32,8 @@ version.workspace = true
 [features]
 # Enable services dashmap support
 services-dashmap = ["opendal/services-dashmap"]
+# Enable services etcd support
+services-etcd = ["opendal/services-etcd"]
 # Enable services ftp support
 services-ftp = ["opendal/services-ftp"]
 # Enable services hdfs support
diff --git a/bin/oli/src/config/mod.rs b/bin/oli/src/config/mod.rs
index 2a155e436..18f7b818b 100644
--- a/bin/oli/src/config/mod.rs
+++ b/bin/oli/src/config/mod.rs
@@ -170,6 +170,11 @@ impl Config {
                 Operator::from_map::<services::Dashmap>(profile.clone())?.finish(),
                 path,
             )),
+            #[cfg(feature = "services-etcd")]
+            Scheme::Etcd => Ok((
+                Operator::from_map::<services::Etcd>(profile.clone())?.finish(),
+                path,
+            )),
             Scheme::Gcs => Ok((
                 Operator::from_map::<services::Gcs>(profile.clone())?.finish(),
                 path,
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 01159e8b0..33f2d7427 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -113,6 +113,7 @@ services-cos = [
 ]
 services-dashmap = ["dep:dashmap"]
 services-dropbox = []
+services-etcd = ["dep:etcd-client"]
 services-fs = ["tokio/fs"]
 services-ftp = ["dep:suppaftp", "dep:lazy-regex", "dep:bb8", "dep:async-tls"]
 services-gcs = [
@@ -192,6 +193,7 @@ cacache = { version = "11.6", default-features = false, features = [
 chrono = "0.4.26"
 dashmap = { version = "5.4", optional = true }
 dirs = { version = "5.0.1", optional = true }
+etcd-client = { version = "0.11", optional = true, features = ["tls"]}
 flagset = "0.4"
 futures = { version = "0.3", default-features = false, features = ["std"] }
 governor = { version = "0.5", optional = true, features = ["std"] }
diff --git a/core/src/services/etcd/backend.rs b/core/src/services/etcd/backend.rs
new file mode 100644
index 000000000..035e9a58b
--- /dev/null
+++ b/core/src/services/etcd/backend.rs
@@ -0,0 +1,328 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+use std::fmt::Debug;
+use std::fmt::Formatter;
+
+use async_trait::async_trait;
+use etcd_client::Certificate;
+use etcd_client::Error as EtcdError;
+use etcd_client::Identity;
+use etcd_client::TlsOptions;
+use etcd_client::{Client, ConnectOptions, GetOptions};
+use tokio::sync::OnceCell;
+
+use crate::raw::adapters::kv;
+use crate::raw::*;
+use crate::*;
+
+const DEFAULT_ETCD_ENDPOINTS: &str = "http://127.0.0.1:2379";
+
+/// [Etcd](https://etcd.io/) services support.
+#[doc = include_str!("docs.md")]
+#[derive(Clone, Default)]
+pub struct EtcdBuilder {
+    /// network address of the Etcd services.
+    /// If use https, must set TLS options: `ca_path`, `cert_path`, `key_path`.
+    /// e.g. "127.0.0.1:23790,127.0.0.1:23791,127.0.0.1:23792" or "http://127.0.0.1:23790,http://127.0.0.1:23791,http://127.0.0.1:23792" or "https://127.0.0.1:23790,https://127.0.0.1:23791,https://127.0.0.1:23792"
+    ///
+    /// default is "http://127.0.0.1:2379"
+    endpoints: Option<String>,
+    /// the username to connect etcd service.
+    ///
+    /// default is None
+    username: Option<String>,
+    /// the password for authentication
+    ///
+    /// default is None
+    password: Option<String>,
+    /// the working directory of the etcd service. Can be "/path/to/dir"
+    ///
+    /// default is "/"
+    root: Option<String>,
+    /// certificate authority file path
+    ///
+    /// default is None
+    ca_path: Option<String>,
+    /// cert path
+    ///
+    /// default is None
+    cert_path: Option<String>,
+    /// key path
+    ///
+    /// default is None
+    key_path: Option<String>,
+}
+
+impl Debug for EtcdBuilder {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        let mut ds = f.debug_struct("Builder");
+        ds.field("root", &self.root);
+        if let Some(endpoints) = self.endpoints.clone() {
+            ds.field("endpoints", &endpoints);
+        }
+        if let Some(username) = self.username.clone() {
+            ds.field("username", &username);
+        }
+        if self.password.is_some() {
+            ds.field("password", &"<redacted>");
+        }
+        ds.finish()
+    }
+}
+
+impl EtcdBuilder {
+    /// set the network address of etcd service.
+    ///
+    /// default: "http://127.0.0.1:2379"
+    pub fn endpoints(&mut self, endpoints: &str) -> &mut Self {
+        if !endpoints.is_empty() {
+            self.endpoints = Some(endpoints.to_owned());
+        }
+        self
+    }
+
+    /// set the username for etcd
+    ///
+    /// default: no username
+    pub fn username(&mut self, username: &str) -> &mut Self {
+        if !username.is_empty() {
+            self.username = Some(username.to_owned());
+        }
+        self
+    }
+
+    /// set the password for etcd
+    ///
+    /// default: no password
+    pub fn password(&mut self, password: &str) -> &mut Self {
+        if !password.is_empty() {
+            self.password = Some(password.to_owned());
+        }
+        self
+    }
+
+    /// set the working directory, all operations will be performed under it.
+    ///
+    /// default: "/"
+    pub fn root(&mut self, root: &str) -> &mut Self {
+        if !root.is_empty() {
+            self.root = Some(root.to_owned());
+        }
+        self
+    }
+
+    /// Set the certificate authority file path.
+    ///
+    /// default is None
+    pub fn ca_path(&mut self, ca_path: &str) -> &mut Self {
+        if !ca_path.is_empty() {
+            self.ca_path = Some(ca_path.to_string())
+        }
+        self
+    }
+
+    /// Set the certificate file path.
+    ///
+    /// default is None
+    pub fn cert_path(&mut self, cert_path: &str) -> &mut Self {
+        if !cert_path.is_empty() {
+            self.cert_path = Some(cert_path.to_string())
+        }
+        self
+    }
+
+    /// Set the key file path.
+    ///
+    /// default is None
+    pub fn key_path(&mut self, key_path: &str) -> &mut Self {
+        if !key_path.is_empty() {
+            self.key_path = Some(key_path.to_string())
+        }
+        self
+    }
+}
+
+impl Builder for EtcdBuilder {
+    const SCHEME: Scheme = Scheme::Etcd;
+    type Accessor = EtcdBackend;
+
+    fn from_map(map: HashMap<String, String>) -> Self {
+        let mut builder = EtcdBuilder::default();
+
+        map.get("root").map(|v| builder.root(v));
+        map.get("endpoints").map(|v| builder.endpoints(v));
+        map.get("username").map(|v| builder.username(v));
+        map.get("password").map(|v| builder.password(v));
+        map.get("ca_path").map(|v| builder.ca_path(v));
+        map.get("cert_path").map(|v| builder.cert_path(v));
+        map.get("key_path").map(|v| builder.key_path(v));
+
+        builder
+    }
+
+    fn build(&mut self) -> Result<Self::Accessor> {
+        let endpoints = self
+            .endpoints
+            .clone()
+            .unwrap_or_else(|| DEFAULT_ETCD_ENDPOINTS.to_string());
+
+        let endpoints: Vec<String> = endpoints.split(',').map(|s| s.to_string()).collect();
+
+        let mut options = ConnectOptions::new();
+
+        if self.ca_path.is_some() && self.cert_path.is_some() && self.key_path.is_some() {
+            let ca = self.load_pem(self.ca_path.clone().unwrap().as_str())?;
+            let key = self.load_pem(self.key_path.clone().unwrap().as_str())?;
+            let cert = self.load_pem(self.cert_path.clone().unwrap().as_str())?;
+
+            let tls_options = TlsOptions::default()
+                .ca_certificate(Certificate::from_pem(ca))
+                .identity(Identity::from_pem(cert, key));
+            options = options.with_tls(tls_options);
+        }
+
+        if let Some(username) = self.username.clone() {
+            options = options.with_user(username, self.password.clone().unwrap_or("".to_string()));
+        }
+
+        let root = normalize_root(
+            self.root
+                .clone()
+                .unwrap_or_else(|| "/".to_string())
+                .as_str(),
+        );
+
+        let client = OnceCell::new();
+        Ok(EtcdBackend::new(Adapter {
+            root,
+            endpoints,
+            client,
+            options,
+        }))
+    }
+}
+
+impl EtcdBuilder {
+    fn load_pem(&self, path: &str) -> Result<String> {
+        std::fs::read_to_string(path)
+            .map_err(|err| Error::new(ErrorKind::Unexpected, "invalid file path").set_source(err))
+    }
+}
+
+/// Backend for etcd services.
+pub type EtcdBackend = kv::Backend<Adapter>;
+
+#[derive(Clone)]
+pub struct Adapter {
+    root: String,
+    endpoints: Vec<String>,
+    client: OnceCell<Client>,
+    options: ConnectOptions,
+}
+
+// implement `Debug` manually, or password may be leaked.
+impl Debug for Adapter {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        let mut ds = f.debug_struct("Adapter");
+
+        ds.field("root", &self.root);
+        ds.field("endpoints", &self.endpoints.join(","));
+        ds.field("options", &self.options.clone());
+        ds.finish()
+    }
+}
+
+impl Adapter {
+    async fn conn(&self) -> Result<Client> {
+        Ok(self
+            .client
+            .get_or_try_init(|| async {
+                Client::connect(self.endpoints.clone(), Some(self.options.clone())).await
+            })
+            .await?
+            .clone())
+    }
+}
+
+#[async_trait]
+impl kv::Adapter for Adapter {
+    fn metadata(&self) -> kv::Metadata {
+        kv::Metadata::new(
+            Scheme::Etcd,
+            &self.endpoints.join(","),
+            Capability {
+                read: true,
+                write: true,
+                create_dir: true,
+                list: true,
+
+                ..Default::default()
+            },
+        )
+    }
+
+    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
+        let p = build_rooted_abs_path(&self.root, key);
+        let mut client = self.conn().await?;
+        let resp = client.get(p, None).await?;
+        if let Some(kv) = resp.kvs().first() {
+            Ok(Some(kv.value().to_vec()))
+        } else {
+            Ok(None)
+        }
+    }
+
+    async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
+        let p = build_rooted_abs_path(&self.root, key);
+        let mut client = self.conn().await?;
+        let _ = client.put(p, value, None).await?;
+        Ok(())
+    }
+
+    async fn delete(&self, key: &str) -> Result<()> {
+        let p = build_rooted_abs_path(&self.root, key);
+        let mut client = self.conn().await?;
+        let _ = client.delete(p, None).await?;
+        Ok(())
+    }
+
+    async fn scan(&self, path: &str) -> Result<Vec<String>> {
+        let p = build_rooted_abs_path(&self.root, path);
+        let mut client = self.conn().await?;
+        let get_options = Some(GetOptions::new().with_prefix().with_keys_only());
+        let resp = client.get(p, get_options).await?;
+        let mut res = Vec::default();
+        for kv in resp.kvs() {
+            res.push(kv.key_str().map(String::from).map_err(|err| {
+                Error::new(ErrorKind::Unexpected, "store key is not valid utf-8 string")
+                    .set_source(err)
+            })?);
+        }
+
+        Ok(res)
+    }
+}
+
+impl From<EtcdError> for Error {
+    fn from(e: EtcdError) -> Self {
+        Error::new(ErrorKind::Unexpected, e.to_string().as_str())
+            .set_source(e)
+            .set_temporary()
+    }
+}
diff --git a/core/src/services/etcd/docs.md b/core/src/services/etcd/docs.md
new file mode 100644
index 000000000..affbad71d
--- /dev/null
+++ b/core/src/services/etcd/docs.md
@@ -0,0 +1,46 @@
+## Capabilities
+
+This service can be used to:
+
+- [x] stat
+- [x] read
+- [x] write
+- [x] create_dir
+- [x] delete
+- [x] copy
+- [x] rename
+- [ ] ~~list~~
+- [x] scan
+- [ ] ~~presign~~
+- [ ] blocking
+
+## Configuration
+
+- `root`: Set the working directory of `OpenDAL`
+- `endpoints`: Set the network address of etcd servers
+- `username`: Set the username of Redis
+- `password`: Set the password for authentication
+- `ca_path`: Set the ca path to the etcd connection
+- `cert_path`: Set the cert path to the etcd connection
+- `key_path`: Set the key path to the etcd connection
+
+You can refer to [`EtcdBuilder`]'s docs for more information
+
+## Example
+
+### Via Builder
+
+```rust
+use anyhow::Result;
+use opendal::services::Etcd;
+use opendal::Operator;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let mut builder = Etcd::default();
+
+    // this will build a Operator accessing etcd which runs on http://127.0.0.1:2379
+    let op: Operator = Operator::new(builder)?.finish();
+    Ok(())
+}
+```
diff --git a/core/src/services/etcd/mod.rs b/core/src/services/etcd/mod.rs
new file mode 100644
index 000000000..c68fce5f8
--- /dev/null
+++ b/core/src/services/etcd/mod.rs
@@ -0,0 +1,19 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+mod backend;
+pub use backend::EtcdBuilder as Etcd;
diff --git a/core/src/services/mod.rs b/core/src/services/mod.rs
index b484fcc30..faa541629 100644
--- a/core/src/services/mod.rs
+++ b/core/src/services/mod.rs
@@ -39,6 +39,11 @@ mod dashmap;
 #[cfg(feature = "services-dashmap")]
 pub use self::dashmap::Dashmap;
 
+#[cfg(feature = "services-etcd")]
+mod etcd;
+#[cfg(feature = "services-etcd")]
+pub use self::etcd::Etcd;
+
 #[cfg(feature = "services-fs")]
 mod fs;
 #[cfg(feature = "services-fs")]
diff --git a/core/src/types/operator/builder.rs b/core/src/types/operator/builder.rs
index 3f78d3ced..1f5e88d3f 100644
--- a/core/src/types/operator/builder.rs
+++ b/core/src/types/operator/builder.rs
@@ -163,6 +163,8 @@ impl Operator {
             Scheme::Cos => Self::from_map::<services::Cos>(map)?.finish(),
             #[cfg(feature = "services-dashmap")]
             Scheme::Dashmap => Self::from_map::<services::Dashmap>(map)?.finish(),
+            #[cfg(feature = "services-etcd")]
+            Scheme::Etcd => Self::from_map::<services::Etcd>(map)?.finish(),
             #[cfg(feature = "services-fs")]
             Scheme::Fs => Self::from_map::<services::Fs>(map)?.finish(),
             #[cfg(feature = "services-ftp")]
diff --git a/core/src/types/scheme.rs b/core/src/types/scheme.rs
index 475732e7b..3b29260c9 100644
--- a/core/src/types/scheme.rs
+++ b/core/src/types/scheme.rs
@@ -41,6 +41,8 @@ pub enum Scheme {
     Cos,
     /// [dashmap][crate::services::Dashmap]: dashmap backend support.
     Dashmap,
+    /// [etcd][crate::services::Etcd]: Etcd Services
+    Etcd,
     /// [fs][crate::services::Fs]: POSIX alike file system.
     Fs,
     /// [ftp][crate::services::Ftp]: FTP backend.
@@ -139,6 +141,7 @@ impl FromStr for Scheme {
             "cacache" => Ok(Scheme::Cacache),
             "cos" => Ok(Scheme::Cos),
             "dashmap" => Ok(Scheme::Dashmap),
+            "etcd" => Ok(Scheme::Etcd),
             "fs" => Ok(Scheme::Fs),
             "gcs" => Ok(Scheme::Gcs),
             "ghac" => Ok(Scheme::Ghac),
@@ -176,6 +179,7 @@ impl From<Scheme> for &'static str {
             Scheme::Cacache => "cacache",
             Scheme::Cos => "cos",
             Scheme::Dashmap => "dashmap",
+            Scheme::Etcd => "etcd",
             Scheme::Fs => "fs",
             Scheme::Gcs => "gcs",
             Scheme::Ghac => "ghac",