use super::{
CLOUD_ENGINE_NODE_INCLUDED_STATUSES, CLOUD_ENGINE_NODE_REWARD_TYPE, CloudEngineNodeInfoReport,
CloudEngineNodeInfoRequest, CloudEngineNodeInfoSourceData, CloudEngineNodeListReport,
CloudEngineNodeListRequest, CloudEngineNodeListSourceData, CloudEngineNodeRow,
MAX_CLOUD_ENGINE_NODE_ROWS,
};
use crate::ic::{
IcHostError, IcSourceRequest, LiveIcSource, canonical_request_principal,
canonicalize_node_status_rows_with_policy, dashboard_source_request, invalid_source,
invalid_source_value, node_status_counts, report_provenance, validate_dashboard_network,
validate_provenance,
};
use std::collections::HashSet;
pub trait CloudEngineNodeSource {
fn fetch_cloud_engine_node_list(
&self,
request: &IcSourceRequest,
node_provider_id: Option<&str>,
) -> Result<CloudEngineNodeListSourceData, IcHostError>;
fn fetch_cloud_engine_node_info(
&self,
request: &IcSourceRequest,
node_id: &str,
) -> Result<CloudEngineNodeInfoSourceData, IcHostError>;
}
pub fn build_cloud_engine_node_list_report(
request: &CloudEngineNodeListRequest,
) -> Result<CloudEngineNodeListReport, IcHostError> {
build_cloud_engine_node_list_report_with_source(request, &LiveIcSource)
}
pub fn build_cloud_engine_node_list_report_with_source(
request: &CloudEngineNodeListRequest,
source: &dyn CloudEngineNodeSource,
) -> Result<CloudEngineNodeListReport, IcHostError> {
validate_dashboard_network(&request.network)?;
let requested_node_provider_id = request
.node_provider_id
.as_deref()
.map(|value| canonical_request_principal("node_provider_id", value))
.transpose()?;
let expected_source = dashboard_source_request(&request.source_endpoint, request.now_unix_secs);
let mut source_data = source
.fetch_cloud_engine_node_list(&expected_source, requested_node_provider_id.as_deref())?;
validate_provenance(&expected_source, &source_data.source)?;
if source_data.requested_node_provider_id != requested_node_provider_id {
return invalid_source(format!(
"node-provider filter is {:?}, expected {:?}",
source_data.requested_node_provider_id, requested_node_provider_id
));
}
if source_data.node_reward_type != CLOUD_ENGINE_NODE_REWARD_TYPE {
return invalid_source(format!(
"node reward-type scope is {:?}, expected {CLOUD_ENGINE_NODE_REWARD_TYPE:?}",
source_data.node_reward_type
));
}
let expected_statuses = included_statuses();
if source_data.included_statuses != expected_statuses {
return invalid_source(format!(
"included status scope is {:?}, expected {:?}",
source_data.included_statuses, expected_statuses
));
}
canonicalize_node_status_rows_with_policy(
&mut source_data.nodes,
MAX_CLOUD_ENGINE_NODE_ROWS,
false,
)
.map_err(invalid_source_value)?;
validate_type4_nodes(
&source_data.nodes,
requested_node_provider_id.as_deref(),
true,
)?;
let node_providers = source_data
.nodes
.iter()
.map(|node| node.node_provider_id.as_str())
.collect::<HashSet<_>>();
let cloud_engine_subnets = source_data
.nodes
.iter()
.filter_map(|node| node.cloud_engine_subnet_id.as_deref())
.collect::<HashSet<_>>();
let unassigned_cloud_engine_node_count = source_data
.nodes
.iter()
.filter(|node| node.cloud_engine_subnet_id.is_none())
.count();
Ok(CloudEngineNodeListReport {
provenance: report_provenance(source_data.source),
node_reward_type: source_data.node_reward_type,
included_statuses: source_data.included_statuses,
requested_node_provider_id,
node_count: source_data.nodes.len(),
status_counts: node_status_counts(source_data.nodes.iter()),
node_provider_count: node_providers.len(),
cloud_engine_subnet_count: cloud_engine_subnets.len(),
unassigned_cloud_engine_node_count,
nodes: source_data.nodes,
})
}
pub fn build_cloud_engine_node_info_report(
request: &CloudEngineNodeInfoRequest,
) -> Result<CloudEngineNodeInfoReport, IcHostError> {
build_cloud_engine_node_info_report_with_source(request, &LiveIcSource)
}
pub fn build_cloud_engine_node_info_report_with_source(
request: &CloudEngineNodeInfoRequest,
source: &dyn CloudEngineNodeSource,
) -> Result<CloudEngineNodeInfoReport, IcHostError> {
validate_dashboard_network(&request.network)?;
let requested_node_id = canonical_request_principal("node_id", &request.node_id)?;
let expected_source = dashboard_source_request(&request.source_endpoint, request.now_unix_secs);
let source_data = source.fetch_cloud_engine_node_info(&expected_source, &requested_node_id)?;
validate_provenance(&expected_source, &source_data.source)?;
if source_data.node_id != requested_node_id {
return invalid_source(format!(
"node target is {:?}, expected {requested_node_id:?}",
source_data.node_id
));
}
let mut nodes = vec![source_data.node];
canonicalize_node_status_rows_with_policy(&mut nodes, 1, true).map_err(invalid_source_value)?;
validate_type4_nodes(&nodes, None, false)?;
let node = nodes
.into_iter()
.next()
.ok_or_else(|| invalid_source_value("exact node source returned no row"))?;
if node.node_id != requested_node_id {
return invalid_source(format!(
"returned node id is {:?}, expected {requested_node_id:?}",
node.node_id
));
}
Ok(CloudEngineNodeInfoReport {
provenance: report_provenance(source_data.source),
node,
})
}
fn validate_type4_nodes(
nodes: &[CloudEngineNodeRow],
requested_node_provider_id: Option<&str>,
require_included_status: bool,
) -> Result<(), IcHostError> {
for node in nodes {
if node.node_reward_type != CLOUD_ENGINE_NODE_REWARD_TYPE {
return invalid_source(format!(
"node {} reward type is {:?}, expected {CLOUD_ENGINE_NODE_REWARD_TYPE:?}",
node.node_id, node.node_reward_type
));
}
if require_included_status
&& !CLOUD_ENGINE_NODE_INCLUDED_STATUSES.contains(&node.status.as_str())
{
return invalid_source(format!(
"node {} status {:?} was not in the explicitly requested scope",
node.node_id, node.status
));
}
if let Some(expected_provider) = requested_node_provider_id
&& node.node_provider_id != expected_provider
{
return invalid_source(format!(
"node {} provider is {:?}, expected requested provider {expected_provider:?}",
node.node_id, node.node_provider_id
));
}
}
Ok(())
}
fn included_statuses() -> Vec<String> {
CLOUD_ENGINE_NODE_INCLUDED_STATUSES
.into_iter()
.map(str::to_string)
.collect()
}