Skip to content

feat(grpc-xds): gRFC A74 model validated xDS resources and XdsConfig snapshot - #2775

Open
YutaoMa wants to merge 3 commits into
grpc:masterfrom
YutaoMa:yutaoma/grpc-xds-config
Open

feat(grpc-xds): gRFC A74 model validated xDS resources and XdsConfig snapshot#2775
YutaoMa wants to merge 3 commits into
grpc:masterfrom
YutaoMa:yutaoma/grpc-xds-config

Conversation

@YutaoMa

@YutaoMa YutaoMa commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Motivation

Ref: #2754

Per gRFC A74, to avoid config tear across xDS layers, all watches (LDS, RDS, CDS, EDS) are moved into a single resolver behind an XdsDependencyManager that emits a single complete XdsConfig.

Solution

This PR implements the XdsConfig modeling, along with its xDS resource types(the validated types corresponding to the Envoy proto types) and validation logic. This PR is a pre-req to the XdsDependencyManager.

The shape of XdsConfig closely mirrors gRFC A74's C++ sketch, as well as other gRPC implementations such as grpc-go. A few design choices were made for Rust ergonomics, such as using an index to store selected virtual host rather than a reference to avoid self-referential borrow.

@YutaoMa
YutaoMa marked this pull request as ready for review July 28, 2026 22:47
@YutaoMa

YutaoMa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Hi @ejona86 @dfawley looking for reviews on this XdsConfig PR. Thanks!

@dfawley dfawley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks great overall, thanks! I've found a handful of things that should be fixed, but it should all be pretty small.

/// The atomic xDS configuration snapshot for a channel.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub(crate) struct XdsConfig {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am assuming this would, itself, end up in an Arc, so that it can be shared between the name resolver and all the LB policies? If so do we need Arcs on the individual fields as well?

"sum of weighted cluster weights exceeds {}",
u32::MAX
)));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think there should be a validation of the total_weight field here:

Suggested change
}
}
if let Some(tw) = wc.total_weight_opt() {
let expected_total = tw.value();
if expected_total == 0 || u64::from(expected_total) != total_weight {
return Err(Error::Validation(format!(
"sum of weighted cluster weights ({total_weight}) does not match explicit total_weight ({expected_total})"
)));
}
}

per route_components.proto:

 // Specifies the total weight across all clusters. The sum of all cluster weights must equal this
 // value, which must be greater than 0. Defaults to 100.
 google.protobuf.UInt32Value total_weight = 3 [(validate.rules).uint32 = {gte: 1}];

and A28:

  • Can be Weighted_clusters
    • The sum of weights must add up to the total_weight.

&& !Arc::ptr_eq(inline, &route_config)
{
return None;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also validate that if route_source is RouteSource::Rds, that the rds name matches the route_config name?

}
let any: Any = api_listener.api_listener().to_owned();

let hcm = HttpConnectionManager::parse(any.value()).map_err(|e| {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should validate any's type_url before parsing this to ensure the message type is what we expect.

}

fn validate_header_matcher(hm: HeaderMatcherView<'_>) -> xds_client::Result<HeaderMatcher> {
let name = hm.name().to_str().unwrap_or_default().to_string();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we validate !name.is_empty()?

}

fn validate(message: Self::Message) -> xds_client::Result<Self> {
let name = message.name().to_str().unwrap_or_default().to_string();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we validate !name.is_empty() here as well, matching cluster/endpoint validation?

// DEGRADED and anything unrecognized fall back to Unknown below.
EnvoyHealthStatus::Unhealthy | EnvoyHealthStatus::Timeout => Self::Unhealthy,
EnvoyHealthStatus::Draining => Self::Draining,
_ => Self::Unknown,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This catch-all maps Degraded to Unknown, and then we will end up using the endpoint since we are supposed to use either HEALTHY or UNKNOWN endpoints per Envoy: https://github.com/envoyproxy/envoy/blob/8f77c4a301bd96d2c7231915e6c3d1f85ea88ae1/api/envoy/config/core/v3/health_check.proto#L36

// of 1,000,000. runtime_key is ignored (gRPC has no runtime config).
let match_fraction = rm.runtime_fraction_opt().and_then(|rf| {
if !rf.has_default_value() {
return None;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If there is no default value set, this should probably error. (The field is required per the protos: https://github.com/envoyproxy/envoy/blob/b67c14052c49890a7e3afe614d50979c346c024b/api/envoy/api/v2/core/base.proto#L368-L369, so this probably won't ever happen in practice)

));
}
let any = custom.typed_config();
let cluster_config = AggregateClusterConfig::parse(any.value()).map_err(|e| {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should also validate any.type_url

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.

2 participants