Skip to content

refactor(etcd): refine config struct and parser#53

Merged
bzp2010 merged 7 commits into
mainfrom
bzp/chore-refine-etcd-config
Apr 21, 2026
Merged

refactor(etcd): refine config struct and parser#53
bzp2010 merged 7 commits into
mainfrom
bzp/chore-refine-etcd-config

Conversation

@bzp2010
Copy link
Copy Markdown
Collaborator

@bzp2010 bzp2010 commented Apr 21, 2026

Summary by CodeRabbit

  • Refactor

    • TLS config now eagerly parses certificate and key material (accepts inline PEM or files), rejects incomplete mTLS inputs at load time, and uses parsed cert/key objects for connections for more robust TLS handling.
  • Bug Fixes

    • Connection validation now rejects missing hosts and enforces consistent URL schemes.
  • Tests

    • Expanded tests to cover PEM parsing, deserialization errors, and partial/invalid TLS inputs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Deserializer now accepts inline PEM or file paths, reads files when provided, parses PEM into OpenSSL objects (CA chain Vec<X509>, client cert chain Vec<X509> and PKey), stores parsed objects in EtcdTlsConfig/EtcdTlsCertConfig, builds TLS from those objects, and validate_connect_config now errors on missing host.

Changes

Cohort / File(s) Summary
Etcd TLS config
src/config/etcd.rs
Replaced serde-flattened enum PEM model with eagerly parsed structs: EtcdTlsCertConfig { certs: Vec<X509>, key: PKey<Private> } and EtcdTlsConfig { ca_cert: Option<Vec<X509>>, client_cert: Option<EtcdTlsCertConfig>, insecure_skip_verify: bool }. Added custom Deserialize to accept inline PEM or *_file paths (reads files if present), parse PEM into X509 chains and keys, error on partial cert/key pairs, update OpenSSL TLS setup to use parsed certs/keys, and added EtcdConfigError::MissingHost while making validate_connect_config return anyhow::Result.
Tests & fixtures
tests/..., tests/fixtures/*, inlined fixture constants
Updated tests to assert parsed X509 chains and PKey values, included real PEM fixtures via include_str!, switched JSON decoding to serde_json::from_value, removed partial-mTLS completeness test, and added/updated tests for invalid CA PEM, rejecting partial inline/file mTLS inputs, and insecure_skip_verify-only configs.

Sequence Diagram(s)

sequenceDiagram
    participant UserConfig as "User config (JSON/TOML)"
    participant Deserializer as "EtcdTlsConfig::Deserialize"
    participant FS as "File system (optional)"
    participant OpenSSL as "OpenSSL PEM parser"
    participant EtcdClient as "Etcd TLS setup"
    UserConfig->>Deserializer: provide `ca`/`cert`/`key` or `*_file` fields
    Deserializer->>FS: read file paths if `*_file` present
    Deserializer->>OpenSSL: parse PEM into X509 chain(s) and PKey
    OpenSSL-->>Deserializer: return parsed objects
    Deserializer-->>EtcdClient: produce `EtcdTlsConfig` with parsed certs/keys
    EtcdClient->>OpenSSL: load CA certs into store, set client cert/key
    EtcdClient-->>UserConfig: TLS-ready etcd connection config
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error The PR stores sensitive OpenSSL private keys (PKey) in EtcdTlsCertConfig without evidence of custom redacted Debug implementation, risking private key exposure in logs. Implement a custom Debug trait for EtcdTlsCertConfig that redacts sensitive fields (key and certs). Verify no #[derive(Debug)] is applied to structs containing PKey or sensitive material.
E2e Test Quality Review ⚠️ Warning PR modifies critical etcd TLS/mTLS configuration parsing but lacks integration tests validating actual client connections with parsed certificates. Add E2E integration tests using testcontainers to verify parsed X509 certificates and PKey establish valid TLS connections to a test etcd instance.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'refactor(etcd): refine config struct and parser' accurately describes the main change: restructuring the etcd configuration handling by converting enum-based deserialization to struct-based with eagerly parsed OpenSSL materials.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bzp/chore-refine-etcd-config

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/config/etcd.rs (1)

161-176: ⚠️ Potential issue | 🟡 Minor

Reject an empty host list during validation.

host: [] currently passes validation and only fails later in the retry loop, which makes a bad config look like a transient connectivity problem.

Small fix
 pub enum EtcdConfigError {
+    #[error("etcd host list cannot be empty")]
+    EmptyHosts,
+
     #[error("etcd hosts must use a single scheme (all http:// or all https://)")]
     MixedSchemes,
 fn validate_connect_config(config: &Config) -> Result<(), EtcdConfigError> {
+    if config.host.is_empty() {
+        return Err(EtcdConfigError::EmptyHosts);
+    }
+
     let has_https = config.host.iter().any(|h| h.starts_with("https://"));
     let has_http = config.host.iter().any(|h| h.starts_with("http://"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 161 - 176, validate_connect_config currently
allows an empty config.host which should be rejected early; add a check at the
start of validate_connect_config (before checking schemes) that returns a new
EtcdConfigError variant for an empty host list when config.host.is_empty() is
true, and if that variant does not exist add EtcdConfigError::EmptyHostList to
the EtcdConfigError enum (or use a clearly named existing variant) so callers
get a configuration error instead of a connectivity retry loop; keep the rest of
the scheme checks (has_http/has_https and MissingScheme) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/etcd.rs`:
- Around line 80-109: Replace the current use of serde_json::Value in
EtcdTlsConfig::deserialize with a small strongly-typed intermediate struct
(e.g., EtcdTlsRaw { insecure_skip_verify: Option<bool>, ca: Option<String>,
ca_file: Option<String>, cert: Option<String>, cert_file: Option<String>, key:
Option<String>, key_file: Option<String> }) and deserialize into that to force
type validation; then map fields from that struct into EtcdTlsConfig (set
insecure_skip_verify from the Option<bool> or error on wrong type), and when
constructing ca_cert and client_cert use the existing helpers (Self::extract_pem
logic should be adapted to accept either the raw inline string or file path from
the typed struct, then call Self::parse_x509_chain("ca"/"cert", &pem) and
PKey::private_key_from_pem for the key), returning de::Error::custom on
missing/invalid combinations—apply the same pattern for the client_cert branch
used with EtcdTlsCertConfig, Self::parse_x509_chain, and
PKey::private_key_from_pem so type errors like `"insecure_skip_verify": "true"`
fail deserialization instead of silently defaulting.

---

Outside diff comments:
In `@src/config/etcd.rs`:
- Around line 161-176: validate_connect_config currently allows an empty
config.host which should be rejected early; add a check at the start of
validate_connect_config (before checking schemes) that returns a new
EtcdConfigError variant for an empty host list when config.host.is_empty() is
true, and if that variant does not exist add EtcdConfigError::EmptyHostList to
the EtcdConfigError enum (or use a clearly named existing variant) so callers
get a configuration error instead of a connectivity retry loop; keep the rest of
the scheme checks (has_http/has_https and MissingScheme) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54266593-af8f-4402-91fb-7d3942442dd9

📥 Commits

Reviewing files that changed from the base of the PR and between fc1bc22 and 670cc4d.

📒 Files selected for processing (1)
  • src/config/etcd.rs

Comment thread src/config/etcd.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/config/etcd.rs (1)

80-86: ⚠️ Potential issue | 🟠 Major

Fail fast on invalid TLS field types during deserialization.

as_bool().unwrap_or(false) and as_str()-only extraction silently coerce wrong JSON types into defaults/missing values. Example: "insecure_skip_verify": "true" is treated as false instead of erroring.

Suggested fix (typed raw struct)
+#[derive(Deserialize)]
+struct RawEtcdTlsConfig {
+    insecure_skip_verify: Option<bool>,
+    ca: Option<String>,
+    ca_file: Option<String>,
+    cert: Option<String>,
+    cert_file: Option<String>,
+    key: Option<String>,
+    key_file: Option<String>,
+}
+
 impl<'de> Deserialize<'de> for EtcdTlsConfig {
     fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
     where
         D: Deserializer<'de>,
     {
-        let mut config = EtcdTlsConfig::default();
-        let value = serde_json::Value::deserialize(deserializer)?;
+        let raw = RawEtcdTlsConfig::deserialize(deserializer)?;
+        let mut config = EtcdTlsConfig::default();

-        config.insecure_skip_verify = value
-            .get("insecure_skip_verify")
-            .and_then(|v| v.as_bool())
-            .unwrap_or(false);
+        config.insecure_skip_verify = raw.insecure_skip_verify.unwrap_or(false);
         // map remaining fields from typed options...
In serde_json, what does `serde_json::Value::as_bool()` return when the JSON value is a string, e.g. `"true"`?

Also applies to: 95-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 80 - 86, The deserializer for EtcdTlsConfig
is silently accepting wrong JSON types (e.g., "insecure_skip_verify": "true")
via as_bool()/as_str() and unwrap_or, so change it to validate types and fail
fast: when deserializing inside the function that builds EtcdTlsConfig (the
block using EtcdTlsConfig::default and Value::deserialize(deserializer)),
replace the as_bool().unwrap_or(false) and any as_str() extractions with
explicit checks that the JSON value exists and is the expected type (matching
Value::get(...).and_then(...) with pattern matching) and return a
serde::de::Error::custom on type mismatch; alternatively parse into a
strongly-typed intermediate struct via serde_json::from_value and map errors to
serde::de errors — apply the same fix for all TLS fields handled in the same
deserializer (the subsequent fields around the current block).
🧹 Nitpick comments (1)
src/config/etcd.rs (1)

127-138: Reject ambiguous label + label_file combinations explicitly.

If both inline PEM and file path are provided for the same field, current logic silently prefers inline content. Returning an explicit error makes config intent unambiguous and easier to debug.

Suggested direction
-fn extract_pem(label: &str, value: &serde_json::Value) -> Option<Result<String>> {
-    if let Some(s) = value.get(label).and_then(|v| v.as_str()) {
-        Some(Ok(s.to_string()))
-    } else {
-        value
-            .get(format!("{label}_file"))
-            .and_then(|v| v.as_str())
-            .map(|path| {
-                std::fs::read_to_string(path)
-                    .with_context(|| format!("failed to read {label} file \"{path}\""))
-            })
-    }
+fn extract_pem(label: &str, value: &serde_json::Value) -> Result<Option<String>> {
+    let inline = value.get(label).and_then(|v| v.as_str()).map(str::to_owned);
+    let file = value
+        .get(format!("{label}_file"))
+        .and_then(|v| v.as_str())
+        .map(str::to_owned);
+
+    match (inline, file) {
+        (Some(_), Some(_)) => Err(anyhow!("provide either {label} or {label}_file, not both")),
+        (Some(pem), None) => Ok(Some(pem)),
+        (None, Some(path)) => Ok(Some(
+            std::fs::read_to_string(&path)
+                .with_context(|| format!("failed to read {label} file \"{path}\""))?,
+        )),
+        (None, None) => Ok(None),
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 127 - 138, In extract_pem, explicitly detect
when both inline and "{label}_file" are provided and return Some(Err(...))
instead of silently preferring the inline value; inside the function (refer to
extract_pem and the `label` variable) check value.get(label).and_then(|v|
v.as_str()) and value.get(format!("{label}_file")).and_then(|v| v.as_str()) — if
both are Some, return Some(Err(anyhow::anyhow!("both {label} and {label}_file
provided, please supply only one"))) so the caller sees an explicit error;
otherwise preserve the existing behavior for single sources.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/config/etcd.rs`:
- Around line 80-86: The deserializer for EtcdTlsConfig is silently accepting
wrong JSON types (e.g., "insecure_skip_verify": "true") via as_bool()/as_str()
and unwrap_or, so change it to validate types and fail fast: when deserializing
inside the function that builds EtcdTlsConfig (the block using
EtcdTlsConfig::default and Value::deserialize(deserializer)), replace the
as_bool().unwrap_or(false) and any as_str() extractions with explicit checks
that the JSON value exists and is the expected type (matching
Value::get(...).and_then(...) with pattern matching) and return a
serde::de::Error::custom on type mismatch; alternatively parse into a
strongly-typed intermediate struct via serde_json::from_value and map errors to
serde::de errors — apply the same fix for all TLS fields handled in the same
deserializer (the subsequent fields around the current block).

---

Nitpick comments:
In `@src/config/etcd.rs`:
- Around line 127-138: In extract_pem, explicitly detect when both inline and
"{label}_file" are provided and return Some(Err(...)) instead of silently
preferring the inline value; inside the function (refer to extract_pem and the
`label` variable) check value.get(label).and_then(|v| v.as_str()) and
value.get(format!("{label}_file")).and_then(|v| v.as_str()) — if both are Some,
return Some(Err(anyhow::anyhow!("both {label} and {label}_file provided, please
supply only one"))) so the caller sees an explicit error; otherwise preserve the
existing behavior for single sources.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 460cedb3-d04f-45a0-b650-4d0386751361

📥 Commits

Reviewing files that changed from the base of the PR and between 670cc4d and 4984ecf.

📒 Files selected for processing (1)
  • src/config/etcd.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/config/etcd.rs (1)

35-43: Consider returning references instead of cloning in accessors.

The certs() and key() methods clone their contents on every call. While this works, returning references (&[X509] and &PKey<Private>) would be more idiomatic and efficient. The current callers in connect_client can clone where needed.

♻️ Suggested refactor
-    /// Return the parsed client certificate chain.
-    pub fn certs(&self) -> Vec<X509> {
-        self.certs.clone()
+    /// Return the parsed client certificate chain.
+    pub fn certs(&self) -> &[X509] {
+        &self.certs
     }

-    /// Return the parsed client private key.
-    pub fn key(&self) -> PKey<pkey::Private> {
-        self.key.clone()
+    /// Return the parsed client private key.
+    pub fn key(&self) -> &PKey<pkey::Private> {
+        &self.key
     }

Then update the usage in connect_client:

-                    let cert = client_cert.certs();
-                    let key = client_cert.key();
+                    let cert = client_cert.certs().to_vec();
+                    let key = client_cert.key().clone();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/etcd.rs` around lines 35 - 43, Change the accessors to return
references instead of cloning: modify certs() to return &[X509] (or &Vec<X509>)
and key() to return &PKey<pkey::Private>, and update all call sites (notably
connect_client) to clone only when they actually need owned values; keep the
existing function names (certs, key) and types (X509, PKey<pkey::Private>) so
you can locate them easily and replace the cloning at call sites with explicit
.clone() where ownership is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/config/etcd.rs`:
- Around line 35-43: Change the accessors to return references instead of
cloning: modify certs() to return &[X509] (or &Vec<X509>) and key() to return
&PKey<pkey::Private>, and update all call sites (notably connect_client) to
clone only when they actually need owned values; keep the existing function
names (certs, key) and types (X509, PKey<pkey::Private>) so you can locate them
easily and replace the cloning at call sites with explicit .clone() where
ownership is required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 95d768f0-4054-41d1-a657-5b956219ddff

📥 Commits

Reviewing files that changed from the base of the PR and between 4984ecf and 93c13c4.

📒 Files selected for processing (1)
  • src/config/etcd.rs

@bzp2010 bzp2010 changed the title feat(etcd): refine config struct and parser refactor(etcd): refine config struct and parser Apr 21, 2026
@bzp2010 bzp2010 merged commit 8dcad6a into main Apr 21, 2026
10 checks passed
@bzp2010 bzp2010 deleted the bzp/chore-refine-etcd-config branch April 21, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant