Tighten cluster builds and add record-set DNS publication
This commit is contained in:
parent
b75766af0b
commit
aba1b73d5b
22 changed files with 925 additions and 201 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
target-dir = "work/cargo-target"
|
||||||
11
.github/workflows/nix.yml
vendored
11
.github/workflows/nix.yml
vendored
|
|
@ -17,6 +17,7 @@ jobs:
|
||||||
any_changed: ${{ steps.detect.outputs.any_changed }}
|
any_changed: ${{ steps.detect.outputs.any_changed }}
|
||||||
build_changed: ${{ steps.detect.outputs.build_changed }}
|
build_changed: ${{ steps.detect.outputs.build_changed }}
|
||||||
global_changed: ${{ steps.detect.outputs.global_changed }}
|
global_changed: ${{ steps.detect.outputs.global_changed }}
|
||||||
|
shared_crates: ${{ steps.detect.outputs.shared_crates }}
|
||||||
shared_crates_changed: ${{ steps.detect.outputs.shared_crates_changed }}
|
shared_crates_changed: ${{ steps.detect.outputs.shared_crates_changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -81,15 +82,19 @@ jobs:
|
||||||
needs: filter
|
needs: filter
|
||||||
if: ${{ needs.filter.outputs.shared_crates_changed == 'true' }}
|
if: ${{ needs.filter.outputs.shared_crates_changed == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: gate (shared crates)
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
crate: ${{ fromJSON(needs.filter.outputs.shared_crates) }}
|
||||||
|
name: gate (shared crate: ${{ matrix.crate }})
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: DeterminateSystems/nix-installer-action@v11
|
- uses: DeterminateSystems/nix-installer-action@v11
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||||
|
|
||||||
- name: Run Shared Crates Gate
|
- name: Run Shared Crate Gate
|
||||||
run: |
|
run: |
|
||||||
nix run ./nix/ci#gate-ci -- --shared-crates --tier 0 --no-logs
|
nix run ./nix/ci#gate-ci -- --shared-crate ${{ matrix.crate }} --tier 0 --no-logs
|
||||||
|
|
||||||
# Build server packages (tier 1+)
|
# Build server packages (tier 1+)
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -3,9 +3,11 @@
|
||||||
|
|
||||||
.PHONY: all build cluster-up cluster-down cluster-status cluster-validate cluster-smoke cluster-matrix cluster-bench-storage clean
|
.PHONY: all build cluster-up cluster-down cluster-status cluster-validate cluster-smoke cluster-matrix cluster-bench-storage clean
|
||||||
|
|
||||||
# Build all services (using Nix)
|
PACKAGE ?= default
|
||||||
|
|
||||||
|
# Build a single package by default; set PACKAGE=default to build the full bundle.
|
||||||
build:
|
build:
|
||||||
nix build .#packages.x86_64-linux.default
|
nix build .#$(PACKAGE)
|
||||||
|
|
||||||
# Cluster Management
|
# Cluster Management
|
||||||
cluster-up:
|
cluster-up:
|
||||||
|
|
|
||||||
|
|
@ -496,6 +496,8 @@ pub enum DnsPublishMode {
|
||||||
LoadBalancer,
|
LoadBalancer,
|
||||||
/// Publish the first healthy instance IP directly.
|
/// Publish the first healthy instance IP directly.
|
||||||
Direct,
|
Direct,
|
||||||
|
/// Publish all healthy instance IPs directly as a DNS record set.
|
||||||
|
DirectMulti,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Desired DNS publication for a service.
|
/// Desired DNS publication for a service.
|
||||||
|
|
@ -894,9 +896,15 @@ pub struct PublishedLoadBalancerState {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct PublishedDnsRecordState {
|
pub struct PublishedDnsRecordState {
|
||||||
pub zone_id: String,
|
pub zone_id: String,
|
||||||
|
#[serde(default)]
|
||||||
pub record_id: String,
|
pub record_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub record_ids: Vec<String>,
|
||||||
pub fqdn: String,
|
pub fqdn: String,
|
||||||
|
#[serde(default)]
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub values: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observed publication state stored separately from ServiceSpec desired state.
|
/// Observed publication state stored separately from ServiceSpec desired state.
|
||||||
|
|
@ -1215,8 +1223,10 @@ mod tests {
|
||||||
dns: Some(PublishedDnsRecordState {
|
dns: Some(PublishedDnsRecordState {
|
||||||
zone_id: "zone-1".to_string(),
|
zone_id: "zone-1".to_string(),
|
||||||
record_id: "record-1".to_string(),
|
record_id: "record-1".to_string(),
|
||||||
|
record_ids: vec!["record-1".to_string()],
|
||||||
fqdn: "api.test.cluster.local".to_string(),
|
fqdn: "api.test.cluster.local".to_string(),
|
||||||
value: "10.0.0.50".to_string(),
|
value: "10.0.0.50".to_string(),
|
||||||
|
values: vec!["10.0.0.50".to_string()],
|
||||||
}),
|
}),
|
||||||
observed_at: None,
|
observed_at: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chainfire_client::Client;
|
use chainfire_client::Client;
|
||||||
|
|
@ -354,24 +354,33 @@ impl PublicationController {
|
||||||
return Ok(existing.cloned());
|
return Ok(existing.cloned());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(value) = desired_dns_value(spec, healthy_instances, load_balancer) else {
|
let desired_values = desired_dns_values(spec, healthy_instances, load_balancer);
|
||||||
|
if desired_values.is_empty() {
|
||||||
if let Some(existing) = existing {
|
if let Some(existing) = existing {
|
||||||
self.cleanup_dns(auth_token, existing).await?;
|
self.cleanup_dns(auth_token, existing).await?;
|
||||||
}
|
}
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
}
|
||||||
|
|
||||||
let zone_name = normalize_zone_name(&spec.zone);
|
let zone_name = normalize_zone_name(&spec.zone);
|
||||||
let record_name = record_name_for_service(spec, service);
|
let record_name = record_name_for_service(spec, service);
|
||||||
let fqdn = format!("{}.{}", record_name, zone_name);
|
let fqdn = format!("{}.{}", record_name, zone_name);
|
||||||
|
let primary_value = desired_values.first().cloned().unwrap_or_default();
|
||||||
|
|
||||||
if self.config.dry_run {
|
if self.config.dry_run {
|
||||||
info!(service = %service.name, fqdn = %fqdn, value = %value, "would reconcile native DNS record");
|
info!(
|
||||||
|
service = %service.name,
|
||||||
|
fqdn = %fqdn,
|
||||||
|
values = ?desired_values,
|
||||||
|
"would reconcile native DNS record set"
|
||||||
|
);
|
||||||
return Ok(existing.cloned().or(Some(PublishedDnsRecordState {
|
return Ok(existing.cloned().or(Some(PublishedDnsRecordState {
|
||||||
zone_id: String::new(),
|
zone_id: String::new(),
|
||||||
record_id: String::new(),
|
record_id: String::new(),
|
||||||
|
record_ids: Vec::new(),
|
||||||
fqdn,
|
fqdn,
|
||||||
value,
|
value: primary_value,
|
||||||
|
values: desired_values,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,22 +389,28 @@ impl PublicationController {
|
||||||
|
|
||||||
let zone =
|
let zone =
|
||||||
ensure_zone(&mut zone_client, auth_token, &zone_name, org_id, project_id).await?;
|
ensure_zone(&mut zone_client, auth_token, &zone_name, org_id, project_id).await?;
|
||||||
let record = ensure_record(
|
let records = ensure_records(
|
||||||
&mut record_client,
|
&mut record_client,
|
||||||
auth_token,
|
auth_token,
|
||||||
existing,
|
existing,
|
||||||
&zone.id,
|
&zone.id,
|
||||||
&record_name,
|
&record_name,
|
||||||
spec.ttl,
|
spec.ttl,
|
||||||
&value,
|
&desired_values,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let record_ids = records
|
||||||
|
.iter()
|
||||||
|
.map(|record| record.id.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(Some(PublishedDnsRecordState {
|
Ok(Some(PublishedDnsRecordState {
|
||||||
zone_id: zone.id,
|
zone_id: zone.id,
|
||||||
record_id: record.id,
|
record_id: record_ids.first().cloned().unwrap_or_default(),
|
||||||
|
record_ids,
|
||||||
fqdn,
|
fqdn,
|
||||||
value,
|
value: primary_value,
|
||||||
|
values: desired_values,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,11 +423,17 @@ impl PublicationController {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let mut record_client = RecordServiceClient::connect(endpoint.clone()).await?;
|
let mut record_client = RecordServiceClient::connect(endpoint.clone()).await?;
|
||||||
|
let mut record_ids = dns_state.record_ids.clone();
|
||||||
|
if record_ids.is_empty() && !dns_state.record_id.is_empty() {
|
||||||
|
record_ids.push(dns_state.record_id.clone());
|
||||||
|
}
|
||||||
|
record_ids.sort();
|
||||||
|
record_ids.dedup();
|
||||||
|
|
||||||
|
for record_id in record_ids {
|
||||||
match record_client
|
match record_client
|
||||||
.delete_record(authorized_request(
|
.delete_record(authorized_request(
|
||||||
DeleteRecordRequest {
|
DeleteRecordRequest { id: record_id },
|
||||||
id: dns_state.record_id.clone(),
|
|
||||||
},
|
|
||||||
auth_token,
|
auth_token,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|
@ -421,6 +442,7 @@ impl PublicationController {
|
||||||
Err(status) if status.code() == Code::NotFound => {}
|
Err(status) if status.code() == Code::NotFound => {}
|
||||||
Err(status) => return Err(status.into()),
|
Err(status) => return Err(status.into()),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -812,15 +834,15 @@ async fn ensure_zone(
|
||||||
.context("FlashDNS returned empty CreateZone response")?)
|
.context("FlashDNS returned empty CreateZone response")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_record(
|
async fn ensure_records(
|
||||||
client: &mut RecordServiceClient<tonic::transport::Channel>,
|
client: &mut RecordServiceClient<tonic::transport::Channel>,
|
||||||
auth_token: &str,
|
auth_token: &str,
|
||||||
existing: Option<&PublishedDnsRecordState>,
|
existing: Option<&PublishedDnsRecordState>,
|
||||||
zone_id: &str,
|
zone_id: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
ttl: u32,
|
ttl: u32,
|
||||||
value: &str,
|
desired_values: &[String],
|
||||||
) -> Result<RecordInfo> {
|
) -> Result<Vec<RecordInfo>> {
|
||||||
let records = client
|
let records = client
|
||||||
.list_records(authorized_request(
|
.list_records(authorized_request(
|
||||||
ListRecordsRequest {
|
ListRecordsRequest {
|
||||||
|
|
@ -839,23 +861,38 @@ async fn ensure_record(
|
||||||
let mut matching = records
|
let mut matching = records
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|record| {
|
.filter(|record| {
|
||||||
existing.map(|state| state.record_id.as_str()) == Some(record.id.as_str())
|
record.name == name
|
||||||
|| record.name == name
|
|| existing.map(|state| state.record_id.as_str()) == Some(record.id.as_str())
|
||||||
|
|| existing
|
||||||
|
.map(|state| state.record_ids.iter().any(|id| id == &record.id))
|
||||||
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
matching.sort_by(|lhs, rhs| {
|
||||||
if let Some(record) = matching.first().cloned() {
|
record_a_value(lhs)
|
||||||
let record_value = record
|
.cmp(&record_a_value(rhs))
|
||||||
.data
|
.then_with(|| lhs.id.cmp(&rhs.id))
|
||||||
.as_ref()
|
|
||||||
.and_then(|data| data.data.as_ref())
|
|
||||||
.and_then(|data| match data {
|
|
||||||
record_data::Data::A(record) => Some(record.address.clone()),
|
|
||||||
_ => None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if record_value.as_deref() != Some(value) || record.ttl != ttl {
|
let mut records_by_value: BTreeMap<String, Vec<RecordInfo>> = BTreeMap::new();
|
||||||
|
for record in matching {
|
||||||
|
let Some(value) = record_a_value(&record) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
records_by_value.entry(value).or_default().push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ensured = Vec::new();
|
||||||
|
for desired_value in desired_values {
|
||||||
|
if let Some(record) = records_by_value.get_mut(desired_value).and_then(|records| {
|
||||||
|
if records.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(records.remove(0))
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if record.ttl != ttl || !record.enabled {
|
||||||
let updated = client
|
let updated = client
|
||||||
.update_record(authorized_request(
|
.update_record(authorized_request(
|
||||||
UpdateRecordRequest {
|
UpdateRecordRequest {
|
||||||
|
|
@ -863,7 +900,7 @@ async fn ensure_record(
|
||||||
ttl: Some(ttl),
|
ttl: Some(ttl),
|
||||||
data: Some(RecordData {
|
data: Some(RecordData {
|
||||||
data: Some(record_data::Data::A(ARecord {
|
data: Some(record_data::Data::A(ARecord {
|
||||||
address: value.to_string(),
|
address: desired_value.to_string(),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
enabled: Some(true),
|
enabled: Some(true),
|
||||||
|
|
@ -874,20 +911,15 @@ async fn ensure_record(
|
||||||
.into_inner()
|
.into_inner()
|
||||||
.record
|
.record
|
||||||
.context("FlashDNS returned empty UpdateRecord response")?;
|
.context("FlashDNS returned empty UpdateRecord response")?;
|
||||||
matching.remove(0);
|
ensured.push(updated);
|
||||||
for extra in matching {
|
} else {
|
||||||
delete_record(client, auth_token, &extra.id).await?;
|
ensured.push(record);
|
||||||
}
|
}
|
||||||
return Ok(updated);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for extra in matching.into_iter().skip(1) {
|
ensured.push(
|
||||||
delete_record(client, auth_token, &extra.id).await?;
|
client
|
||||||
}
|
|
||||||
return Ok(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(client
|
|
||||||
.create_record(authorized_request(
|
.create_record(authorized_request(
|
||||||
CreateRecordRequest {
|
CreateRecordRequest {
|
||||||
zone_id: zone_id.to_string(),
|
zone_id: zone_id.to_string(),
|
||||||
|
|
@ -896,7 +928,7 @@ async fn ensure_record(
|
||||||
ttl,
|
ttl,
|
||||||
data: Some(RecordData {
|
data: Some(RecordData {
|
||||||
data: Some(record_data::Data::A(ARecord {
|
data: Some(record_data::Data::A(ARecord {
|
||||||
address: value.to_string(),
|
address: desired_value.to_string(),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
@ -905,7 +937,20 @@ async fn ensure_record(
|
||||||
.await?
|
.await?
|
||||||
.into_inner()
|
.into_inner()
|
||||||
.record
|
.record
|
||||||
.context("FlashDNS returned empty CreateRecord response")?)
|
.context("FlashDNS returned empty CreateRecord response")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for extra in records_by_value.into_values().flatten() {
|
||||||
|
delete_record(client, auth_token, &extra.id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensured.sort_by(|lhs, rhs| {
|
||||||
|
record_a_value(lhs)
|
||||||
|
.cmp(&record_a_value(rhs))
|
||||||
|
.then_with(|| lhs.id.cmp(&rhs.id))
|
||||||
|
});
|
||||||
|
Ok(ensured)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_record(
|
async fn delete_record(
|
||||||
|
|
@ -940,23 +985,54 @@ fn resolve_target_port(service: &ServiceSpec, spec: &LoadBalancerPublicationSpec
|
||||||
.or_else(|| service.ports.as_ref().and_then(|ports| ports.grpc))
|
.or_else(|| service.ports.as_ref().and_then(|ports| ports.grpc))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_dns_value(
|
fn record_a_value(record: &RecordInfo) -> Option<String> {
|
||||||
|
record
|
||||||
|
.data
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|data| data.data.as_ref())
|
||||||
|
.and_then(|data| match data {
|
||||||
|
record_data::Data::A(record) => Some(record.address.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_dns_values(values: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||||
|
let mut values = values
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
values.sort();
|
||||||
|
values.dedup();
|
||||||
|
values
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_dns_values(
|
||||||
spec: &DnsPublicationSpec,
|
spec: &DnsPublicationSpec,
|
||||||
healthy_instances: &[ServiceInstanceSpec],
|
healthy_instances: &[ServiceInstanceSpec],
|
||||||
load_balancer: Option<&PublishedLoadBalancerState>,
|
load_balancer: Option<&PublishedLoadBalancerState>,
|
||||||
) -> Option<String> {
|
) -> Vec<String> {
|
||||||
match spec.mode {
|
match spec.mode {
|
||||||
DnsPublishMode::LoadBalancer => load_balancer
|
DnsPublishMode::LoadBalancer => normalize_dns_values(
|
||||||
|
load_balancer
|
||||||
.and_then(|state| state.vip_address.clone())
|
.and_then(|state| state.vip_address.clone())
|
||||||
.filter(|value| !value.is_empty() && value != "0.0.0.0")
|
.filter(|value| !value.is_empty() && value != "0.0.0.0")
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
healthy_instances
|
healthy_instances
|
||||||
.first()
|
.first()
|
||||||
.map(|instance| instance.ip.clone())
|
.map(|instance| instance.ip.clone())
|
||||||
}),
|
})
|
||||||
DnsPublishMode::Direct => healthy_instances
|
.into_iter(),
|
||||||
|
),
|
||||||
|
DnsPublishMode::Direct => normalize_dns_values(
|
||||||
|
healthy_instances
|
||||||
.first()
|
.first()
|
||||||
.map(|instance| instance.ip.clone()),
|
.map(|instance| instance.ip.clone())
|
||||||
|
.into_iter(),
|
||||||
|
),
|
||||||
|
DnsPublishMode::DirectMulti => {
|
||||||
|
normalize_dns_values(healthy_instances.iter().map(|instance| instance.ip.clone()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1153,7 +1229,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dns_value_falls_back_to_healthy_instance_when_vip_missing() {
|
fn test_dns_values_fall_back_to_healthy_instance_when_vip_missing() {
|
||||||
let spec = DnsPublicationSpec {
|
let spec = DnsPublicationSpec {
|
||||||
zone: "native.cluster.test".to_string(),
|
zone: "native.cluster.test".to_string(),
|
||||||
name: Some("api".to_string()),
|
name: Some("api".to_string()),
|
||||||
|
|
@ -1178,8 +1254,73 @@ mod tests {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
desired_dns_value(&spec, &instances, None).as_deref(),
|
desired_dns_values(&spec, &instances, None),
|
||||||
Some("10.0.0.11")
|
vec!["10.0.0.11".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direct_multi_dns_publishes_all_healthy_instance_ips() {
|
||||||
|
let spec = DnsPublicationSpec {
|
||||||
|
zone: "native.cluster.test".to_string(),
|
||||||
|
name: Some("daemon".to_string()),
|
||||||
|
ttl: 60,
|
||||||
|
mode: DnsPublishMode::DirectMulti,
|
||||||
|
};
|
||||||
|
let instances = vec![
|
||||||
|
ServiceInstanceSpec {
|
||||||
|
instance_id: "daemon-node02".to_string(),
|
||||||
|
service: "daemon".to_string(),
|
||||||
|
node_id: "node02".to_string(),
|
||||||
|
ip: "10.0.0.12".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
mesh_port: None,
|
||||||
|
version: None,
|
||||||
|
health_check: None,
|
||||||
|
process: None,
|
||||||
|
container: None,
|
||||||
|
managed_by: None,
|
||||||
|
state: Some("healthy".to_string()),
|
||||||
|
last_heartbeat: None,
|
||||||
|
observed_at: None,
|
||||||
|
},
|
||||||
|
ServiceInstanceSpec {
|
||||||
|
instance_id: "daemon-node01".to_string(),
|
||||||
|
service: "daemon".to_string(),
|
||||||
|
node_id: "node01".to_string(),
|
||||||
|
ip: "10.0.0.11".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
mesh_port: None,
|
||||||
|
version: None,
|
||||||
|
health_check: None,
|
||||||
|
process: None,
|
||||||
|
container: None,
|
||||||
|
managed_by: None,
|
||||||
|
state: Some("healthy".to_string()),
|
||||||
|
last_heartbeat: None,
|
||||||
|
observed_at: None,
|
||||||
|
},
|
||||||
|
ServiceInstanceSpec {
|
||||||
|
instance_id: "daemon-node03".to_string(),
|
||||||
|
service: "daemon".to_string(),
|
||||||
|
node_id: "node03".to_string(),
|
||||||
|
ip: "10.0.0.11".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
mesh_port: None,
|
||||||
|
version: None,
|
||||||
|
health_check: None,
|
||||||
|
process: None,
|
||||||
|
container: None,
|
||||||
|
managed_by: None,
|
||||||
|
state: Some("healthy".to_string()),
|
||||||
|
last_heartbeat: None,
|
||||||
|
observed_at: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
desired_dns_values(&spec, &instances, None),
|
||||||
|
vec!["10.0.0.11".to_string(), "10.0.0.12".to_string()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
276
flake.nix
276
flake.nix
|
|
@ -71,35 +71,158 @@
|
||||||
|
|
||||||
clusterPython = pkgs.python3.withPackages (ps: [ ps.python-snappy ]);
|
clusterPython = pkgs.python3.withPackages (ps: [ ps.python-snappy ]);
|
||||||
|
|
||||||
# Keep package builds stable even when docs or archived assets change.
|
# Keep Rust package builds stable without invalidating every package on
|
||||||
repoSrc = pkgs.lib.cleanSourceWith {
|
# unrelated workspace changes.
|
||||||
|
workspaceSourceRoots = {
|
||||||
|
chainfire = [ "chainfire" ];
|
||||||
|
flaredb = [ "flaredb" ];
|
||||||
|
iam = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
];
|
||||||
|
coronafs = [ "coronafs" ];
|
||||||
|
plasmavmc = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
"lightningstor"
|
||||||
|
"plasmavmc"
|
||||||
|
"prismnet"
|
||||||
|
];
|
||||||
|
prismnet = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
"prismnet"
|
||||||
|
];
|
||||||
|
flashdns = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flashdns"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
];
|
||||||
|
fiberlb = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"fiberlb"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
];
|
||||||
|
lightningstor = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
"lightningstor"
|
||||||
|
];
|
||||||
|
nightlight = [ "nightlight" ];
|
||||||
|
creditservice = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
];
|
||||||
|
apigateway = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"flaredb"
|
||||||
|
"iam"
|
||||||
|
];
|
||||||
|
k8shost = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"fiberlb"
|
||||||
|
"flaredb"
|
||||||
|
"flashdns"
|
||||||
|
"iam"
|
||||||
|
"k8shost"
|
||||||
|
"lightningstor"
|
||||||
|
"plasmavmc"
|
||||||
|
"prismnet"
|
||||||
|
];
|
||||||
|
deployer = [
|
||||||
|
"apigateway"
|
||||||
|
"chainfire"
|
||||||
|
"creditservice"
|
||||||
|
"crates/photon-auth-client"
|
||||||
|
"crates/photon-config"
|
||||||
|
"crates/photon-runtime"
|
||||||
|
"crates/photon-state"
|
||||||
|
"deployer"
|
||||||
|
"fiberlb"
|
||||||
|
"flaredb"
|
||||||
|
"flashdns"
|
||||||
|
"iam"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
mkWorkspaceSrc = workspaceSubdir:
|
||||||
|
let
|
||||||
|
sourceRoots = workspaceSourceRoots.${workspaceSubdir} or [ workspaceSubdir ];
|
||||||
|
in
|
||||||
|
pkgs.lib.cleanSourceWith {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
filter = path: type:
|
filter = path: type:
|
||||||
let
|
let
|
||||||
rel = pkgs.lib.removePrefix ((toString ./. ) + "/") (toString path);
|
rel = pkgs.lib.removePrefix ((toString ./. ) + "/") (toString path);
|
||||||
topLevel = builtins.head (pkgs.lib.splitString "/" rel);
|
|
||||||
includedTopLevels = [
|
|
||||||
"apigateway"
|
|
||||||
"chainfire"
|
|
||||||
"coronafs"
|
|
||||||
"crates"
|
|
||||||
"creditservice"
|
|
||||||
"deployer"
|
|
||||||
"fiberlb"
|
|
||||||
"flashdns"
|
|
||||||
"flaredb"
|
|
||||||
"iam"
|
|
||||||
"k8shost"
|
|
||||||
"lightningstor"
|
|
||||||
"mtls-agent"
|
|
||||||
"nightlight"
|
|
||||||
"plasmavmc"
|
|
||||||
"prismnet"
|
|
||||||
];
|
|
||||||
in
|
in
|
||||||
rel == ""
|
rel == ""
|
||||||
|| builtins.elem rel [ "flake.nix" "flake.lock" ]
|
|| builtins.elem rel [ "flake.nix" "flake.lock" ]
|
||||||
|| builtins.elem topLevel includedTopLevels;
|
|| builtins.any (root:
|
||||||
|
rel == root
|
||||||
|
|| pkgs.lib.hasPrefix "${root}/" rel
|
||||||
|
|| pkgs.lib.hasPrefix "${rel}/" root
|
||||||
|
) sourceRoots;
|
||||||
};
|
};
|
||||||
|
|
||||||
flakeBundleSrc = pkgs.lib.cleanSourceWith {
|
flakeBundleSrc = pkgs.lib.cleanSourceWith {
|
||||||
|
|
@ -440,12 +563,12 @@
|
||||||
# workspaceSubdir: subdirectory containing Cargo.toml (e.g., "chainfire")
|
# workspaceSubdir: subdirectory containing Cargo.toml (e.g., "chainfire")
|
||||||
# mainCrate: optional main crate name if different from workspace
|
# mainCrate: optional main crate name if different from workspace
|
||||||
# description: package description for meta
|
# description: package description for meta
|
||||||
# doCheck: whether to run tests during build (default: true)
|
# doCheck: whether to run tests during build (default: false)
|
||||||
buildRustWorkspace = { name, workspaceSubdir, mainCrate ? null, description ? "", doCheck ? true }:
|
buildRustWorkspace = { name, workspaceSubdir, mainCrate ? null, description ? "", doCheck ? false }:
|
||||||
pkgs.rustPlatform.buildRustPackage ({
|
pkgs.rustPlatform.buildRustPackage ({
|
||||||
pname = name;
|
pname = name;
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = repoSrc;
|
src = mkWorkspaceSrc workspaceSubdir;
|
||||||
|
|
||||||
cargoLock = {
|
cargoLock = {
|
||||||
lockFile = ./${workspaceSubdir}/Cargo.lock;
|
lockFile = ./${workspaceSubdir}/Cargo.lock;
|
||||||
|
|
@ -486,6 +609,43 @@
|
||||||
cargoBuildFlags = [ "-p" mainCrate ];
|
cargoBuildFlags = [ "-p" mainCrate ];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
# Helper function to build multiple binaries from the same workspace in
|
||||||
|
# one cargo invocation. This is mainly used by the VM cluster builds so
|
||||||
|
# a single host build can satisfy several services from the same
|
||||||
|
# workspace.
|
||||||
|
buildRustWorkspaceBundle = { name, workspaceSubdir, crates, description ? "", doCheck ? false }:
|
||||||
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = name;
|
||||||
|
version = "0.1.0";
|
||||||
|
src = mkWorkspaceSrc workspaceSubdir;
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./${workspaceSubdir}/Cargo.lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
buildAndTestSubdir = workspaceSubdir;
|
||||||
|
|
||||||
|
postUnpack = ''
|
||||||
|
cp $sourceRoot/${workspaceSubdir}/Cargo.lock $sourceRoot/Cargo.lock
|
||||||
|
'';
|
||||||
|
|
||||||
|
nativeBuildInputs = commonNativeBuildInputs;
|
||||||
|
buildInputs = commonBuildInputs;
|
||||||
|
|
||||||
|
inherit (commonEnvVars) LIBCLANG_PATH PROTOC ROCKSDB_LIB_DIR;
|
||||||
|
inherit doCheck;
|
||||||
|
|
||||||
|
cargoBuildFlags = pkgs.lib.concatMap (crate: [ "-p" crate ]) crates;
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = description;
|
||||||
|
homepage = "https://github.com/yourorg/plasmacloud";
|
||||||
|
license = licenses.asl20;
|
||||||
|
maintainers = [ ];
|
||||||
|
platforms = platforms.linux;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
|
|
@ -658,6 +818,16 @@
|
||||||
description = "LightningStor distributed storage node daemon";
|
description = "LightningStor distributed storage node daemon";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
lightningstor-workspace = buildRustWorkspaceBundle {
|
||||||
|
name = "lightningstor-workspace";
|
||||||
|
workspaceSubdir = "lightningstor";
|
||||||
|
crates = [
|
||||||
|
"lightningstor-server"
|
||||||
|
"lightningstor-node"
|
||||||
|
];
|
||||||
|
description = "Combined LightningStor server and node workspace build";
|
||||||
|
};
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# NightLight: Prometheus-compatible Metrics Store
|
# NightLight: Prometheus-compatible Metrics Store
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
@ -768,6 +938,20 @@
|
||||||
description = "Label-aware service scheduler for PhotonCloud bare-metal fleets";
|
description = "Label-aware service scheduler for PhotonCloud bare-metal fleets";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deployer-workspace = buildRustWorkspaceBundle {
|
||||||
|
name = "deployer-workspace";
|
||||||
|
workspaceSubdir = "deployer";
|
||||||
|
crates = [
|
||||||
|
"deployer-server"
|
||||||
|
"deployer-ctl"
|
||||||
|
"node-agent"
|
||||||
|
"nix-agent"
|
||||||
|
"plasmacloud-reconciler"
|
||||||
|
"fleet-scheduler"
|
||||||
|
];
|
||||||
|
description = "Combined deployer workspace build for cluster images and checks";
|
||||||
|
};
|
||||||
|
|
||||||
vmClusterDeployerState =
|
vmClusterDeployerState =
|
||||||
self.nixosConfigurations.node01.config.system.build.plasmacloudDeployerClusterState;
|
self.nixosConfigurations.node01.config.system.build.plasmacloudDeployerClusterState;
|
||||||
|
|
||||||
|
|
@ -787,18 +971,12 @@
|
||||||
self.packages.${system}.prismnet-server
|
self.packages.${system}.prismnet-server
|
||||||
self.packages.${system}.flashdns-server
|
self.packages.${system}.flashdns-server
|
||||||
self.packages.${system}.fiberlb-server
|
self.packages.${system}.fiberlb-server
|
||||||
self.packages.${system}.lightningstor-server
|
self.packages.${system}.lightningstor-workspace
|
||||||
self.packages.${system}.lightningstor-node
|
|
||||||
self.packages.${system}.nightlight-server
|
self.packages.${system}.nightlight-server
|
||||||
self.packages.${system}.creditservice-server
|
self.packages.${system}.creditservice-server
|
||||||
self.packages.${system}.apigateway-server
|
self.packages.${system}.apigateway-server
|
||||||
self.packages.${system}.k8shost-server
|
self.packages.${system}.k8shost-server
|
||||||
self.packages.${system}.deployer-server
|
self.packages.${system}.deployer-workspace
|
||||||
self.packages.${system}.deployer-ctl
|
|
||||||
self.packages.${system}.plasmacloud-reconciler
|
|
||||||
self.packages.${system}.nix-agent
|
|
||||||
self.packages.${system}.node-agent
|
|
||||||
self.packages.${system}.fleet-scheduler
|
|
||||||
self.packages.${system}.vmClusterDeployerState
|
self.packages.${system}.vmClusterDeployerState
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
@ -970,9 +1148,9 @@
|
||||||
PHOTONCLOUD_CHAINFIRE_SERVER_BIN =
|
PHOTONCLOUD_CHAINFIRE_SERVER_BIN =
|
||||||
"${self.packages.${system}.chainfire-server}/bin/chainfire";
|
"${self.packages.${system}.chainfire-server}/bin/chainfire";
|
||||||
PHOTONCLOUD_DEPLOYER_SERVER_BIN =
|
PHOTONCLOUD_DEPLOYER_SERVER_BIN =
|
||||||
"${self.packages.${system}.deployer-server}/bin/deployer-server";
|
"${self.packages.${system}.deployer-workspace}/bin/deployer-server";
|
||||||
PHOTONCLOUD_DEPLOYER_CTL_BIN =
|
PHOTONCLOUD_DEPLOYER_CTL_BIN =
|
||||||
"${self.packages.${system}.deployer-ctl}/bin/deployer-ctl";
|
"${self.packages.${system}.deployer-workspace}/bin/deployer-ctl";
|
||||||
} ''
|
} ''
|
||||||
export HOME="$TMPDIR/home"
|
export HOME="$TMPDIR/home"
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
|
|
@ -1007,9 +1185,9 @@
|
||||||
PHOTONCLOUD_CHAINFIRE_SERVER_BIN =
|
PHOTONCLOUD_CHAINFIRE_SERVER_BIN =
|
||||||
"${self.packages.${system}.chainfire-server}/bin/chainfire";
|
"${self.packages.${system}.chainfire-server}/bin/chainfire";
|
||||||
PHOTONCLOUD_DEPLOYER_CTL_BIN =
|
PHOTONCLOUD_DEPLOYER_CTL_BIN =
|
||||||
"${self.packages.${system}.deployer-ctl}/bin/deployer-ctl";
|
"${self.packages.${system}.deployer-workspace}/bin/deployer-ctl";
|
||||||
PHOTONCLOUD_PLASMACLOUD_RECONCILER_BIN =
|
PHOTONCLOUD_PLASMACLOUD_RECONCILER_BIN =
|
||||||
"${self.packages.${system}.plasmacloud-reconciler}/bin/plasmacloud-reconciler";
|
"${self.packages.${system}.deployer-workspace}/bin/plasmacloud-reconciler";
|
||||||
} ''
|
} ''
|
||||||
export HOME="$TMPDIR/home"
|
export HOME="$TMPDIR/home"
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
|
|
@ -1044,11 +1222,11 @@
|
||||||
PHOTONCLOUD_CHAINFIRE_SERVER_BIN =
|
PHOTONCLOUD_CHAINFIRE_SERVER_BIN =
|
||||||
"${self.packages.${system}.chainfire-server}/bin/chainfire";
|
"${self.packages.${system}.chainfire-server}/bin/chainfire";
|
||||||
PHOTONCLOUD_DEPLOYER_CTL_BIN =
|
PHOTONCLOUD_DEPLOYER_CTL_BIN =
|
||||||
"${self.packages.${system}.deployer-ctl}/bin/deployer-ctl";
|
"${self.packages.${system}.deployer-workspace}/bin/deployer-ctl";
|
||||||
PHOTONCLOUD_NODE_AGENT_BIN =
|
PHOTONCLOUD_NODE_AGENT_BIN =
|
||||||
"${self.packages.${system}.node-agent}/bin/node-agent";
|
"${self.packages.${system}.deployer-workspace}/bin/node-agent";
|
||||||
PHOTONCLOUD_FLEET_SCHEDULER_BIN =
|
PHOTONCLOUD_FLEET_SCHEDULER_BIN =
|
||||||
"${self.packages.${system}.fleet-scheduler}/bin/fleet-scheduler";
|
"${self.packages.${system}.deployer-workspace}/bin/fleet-scheduler";
|
||||||
} ''
|
} ''
|
||||||
export HOME="$TMPDIR/home"
|
export HOME="$TMPDIR/home"
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
|
|
@ -1229,19 +1407,21 @@
|
||||||
prismnet-server = self.packages.${final.system}.prismnet-server;
|
prismnet-server = self.packages.${final.system}.prismnet-server;
|
||||||
flashdns-server = self.packages.${final.system}.flashdns-server;
|
flashdns-server = self.packages.${final.system}.flashdns-server;
|
||||||
fiberlb-server = self.packages.${final.system}.fiberlb-server;
|
fiberlb-server = self.packages.${final.system}.fiberlb-server;
|
||||||
lightningstor-server = self.packages.${final.system}.lightningstor-server;
|
lightningstor-workspace = self.packages.${final.system}.lightningstor-workspace;
|
||||||
lightningstor-node = self.packages.${final.system}.lightningstor-node;
|
lightningstor-server = self.packages.${final.system}.lightningstor-workspace;
|
||||||
|
lightningstor-node = self.packages.${final.system}.lightningstor-workspace;
|
||||||
nightlight-server = self.packages.${final.system}.nightlight-server;
|
nightlight-server = self.packages.${final.system}.nightlight-server;
|
||||||
creditservice-server = self.packages.${final.system}.creditservice-server;
|
creditservice-server = self.packages.${final.system}.creditservice-server;
|
||||||
apigateway-server = self.packages.${final.system}.apigateway-server;
|
apigateway-server = self.packages.${final.system}.apigateway-server;
|
||||||
k8shost-server = self.packages.${final.system}.k8shost-server;
|
k8shost-server = self.packages.${final.system}.k8shost-server;
|
||||||
deployer-server = self.packages.${final.system}.deployer-server;
|
deployer-workspace = self.packages.${final.system}.deployer-workspace;
|
||||||
deployer-ctl = self.packages.${final.system}.deployer-ctl;
|
deployer-server = self.packages.${final.system}.deployer-workspace;
|
||||||
plasmacloud-reconciler = self.packages.${final.system}.plasmacloud-reconciler;
|
deployer-ctl = self.packages.${final.system}.deployer-workspace;
|
||||||
|
plasmacloud-reconciler = self.packages.${final.system}.deployer-workspace;
|
||||||
plasmacloudFlakeBundle = self.packages.${final.system}.plasmacloudFlakeBundle;
|
plasmacloudFlakeBundle = self.packages.${final.system}.plasmacloudFlakeBundle;
|
||||||
nix-agent = self.packages.${final.system}.nix-agent;
|
nix-agent = self.packages.${final.system}.deployer-workspace;
|
||||||
node-agent = self.packages.${final.system}.node-agent;
|
node-agent = self.packages.${final.system}.deployer-workspace;
|
||||||
fleet-scheduler = self.packages.${final.system}.fleet-scheduler;
|
fleet-scheduler = self.packages.${final.system}.deployer-workspace;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,9 @@ impl DnsMetadataStore {
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MetadataError::Storage(format!("Failed to initialize Postgres schema: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
MetadataError::Storage(format!("Failed to initialize Postgres schema: {}", e))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +167,9 @@ impl DnsMetadataStore {
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MetadataError::Storage(format!("Failed to initialize SQLite schema: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
MetadataError::Storage(format!("Failed to initialize SQLite schema: {}", e))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,9 +196,7 @@ impl DnsMetadataStore {
|
||||||
.bind(value)
|
.bind(value)
|
||||||
.execute(pool.as_ref())
|
.execute(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| MetadataError::Storage(format!("Postgres put failed: {}", e)))?;
|
||||||
MetadataError::Storage(format!("Postgres put failed: {}", e))
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
SqlStorageBackend::Sqlite(pool) => {
|
SqlStorageBackend::Sqlite(pool) => {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
|
|
@ -395,10 +397,15 @@ impl DnsMetadataStore {
|
||||||
format!("/flashdns/zone_ids/{}", zone_id)
|
format!("/flashdns/zone_ids/{}", zone_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_key(zone_id: &ZoneId, record_name: &str, record_type: RecordType) -> String {
|
fn record_key(
|
||||||
|
zone_id: &ZoneId,
|
||||||
|
record_name: &str,
|
||||||
|
record_type: RecordType,
|
||||||
|
record_id: &RecordId,
|
||||||
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
"/flashdns/records/{}/{}/{}",
|
"/flashdns/records/{}/{}/{}/{}",
|
||||||
zone_id, record_name, record_type
|
zone_id, record_name, record_type, record_id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,6 +413,20 @@ impl DnsMetadataStore {
|
||||||
format!("/flashdns/records/{}/", zone_id)
|
format!("/flashdns/records/{}/", zone_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_type_prefix(zone_id: &ZoneId, record_name: &str, record_type: RecordType) -> String {
|
||||||
|
format!(
|
||||||
|
"/flashdns/records/{}/{}/{}/",
|
||||||
|
zone_id, record_name, record_type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legacy_record_key(zone_id: &ZoneId, record_name: &str, record_type: RecordType) -> String {
|
||||||
|
format!(
|
||||||
|
"/flashdns/records/{}/{}/{}",
|
||||||
|
zone_id, record_name, record_type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn record_id_key(record_id: &RecordId) -> String {
|
fn record_id_key(record_id: &RecordId) -> String {
|
||||||
format!("/flashdns/record_ids/{}", record_id)
|
format!("/flashdns/record_ids/{}", record_id)
|
||||||
}
|
}
|
||||||
|
|
@ -521,7 +542,18 @@ impl DnsMetadataStore {
|
||||||
|
|
||||||
/// Save record
|
/// Save record
|
||||||
pub async fn save_record(&self, record: &Record) -> Result<()> {
|
pub async fn save_record(&self, record: &Record) -> Result<()> {
|
||||||
let key = Self::record_key(&record.zone_id, &record.name, record.record_type);
|
let key = Self::record_key(
|
||||||
|
&record.zone_id,
|
||||||
|
&record.name,
|
||||||
|
record.record_type,
|
||||||
|
&record.id,
|
||||||
|
);
|
||||||
|
let id_key = Self::record_id_key(&record.id);
|
||||||
|
if let Some(existing_key) = self.get(&id_key).await? {
|
||||||
|
if existing_key != key {
|
||||||
|
self.delete_key(&existing_key).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
let value = serde_json::to_string(record).map_err(|e| {
|
let value = serde_json::to_string(record).map_err(|e| {
|
||||||
MetadataError::Serialization(format!("Failed to serialize record: {}", e))
|
MetadataError::Serialization(format!("Failed to serialize record: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -529,29 +561,40 @@ impl DnsMetadataStore {
|
||||||
self.put(&key, &value).await?;
|
self.put(&key, &value).await?;
|
||||||
|
|
||||||
// Also save record ID mapping
|
// Also save record ID mapping
|
||||||
let id_key = Self::record_id_key(&record.id);
|
|
||||||
self.put(&id_key, &key).await?;
|
self.put(&id_key, &key).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load record by name and type
|
/// Load the first record by name and type, preserving compatibility with
|
||||||
|
/// older single-record keys.
|
||||||
pub async fn load_record(
|
pub async fn load_record(
|
||||||
&self,
|
&self,
|
||||||
zone_id: &ZoneId,
|
zone_id: &ZoneId,
|
||||||
record_name: &str,
|
record_name: &str,
|
||||||
record_type: RecordType,
|
record_type: RecordType,
|
||||||
) -> Result<Option<Record>> {
|
) -> Result<Option<Record>> {
|
||||||
let key = Self::record_key(zone_id, record_name, record_type);
|
let prefix = Self::record_type_prefix(zone_id, record_name, record_type);
|
||||||
|
let mut records = self
|
||||||
|
.get_prefix(&prefix)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_, value)| serde_json::from_str::<Record>(&value).ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if let Some(value) = self.get(&key).await? {
|
if records.is_empty() {
|
||||||
|
let legacy_key = Self::legacy_record_key(zone_id, record_name, record_type);
|
||||||
|
if let Some(value) = self.get(&legacy_key).await? {
|
||||||
let record: Record = serde_json::from_str(&value).map_err(|e| {
|
let record: Record = serde_json::from_str(&value).map_err(|e| {
|
||||||
MetadataError::Serialization(format!("Failed to deserialize record: {}", e))
|
MetadataError::Serialization(format!("Failed to deserialize record: {}", e))
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(record))
|
return Ok(Some(record));
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
records.sort_by(|lhs, rhs| lhs.id.to_string().cmp(&rhs.id.to_string()));
|
||||||
|
Ok(records.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load record by ID
|
/// Load record by ID
|
||||||
|
|
@ -574,10 +617,24 @@ impl DnsMetadataStore {
|
||||||
|
|
||||||
/// Delete record
|
/// Delete record
|
||||||
pub async fn delete_record(&self, record: &Record) -> Result<()> {
|
pub async fn delete_record(&self, record: &Record) -> Result<()> {
|
||||||
let key = Self::record_key(&record.zone_id, &record.name, record.record_type);
|
|
||||||
let id_key = Self::record_id_key(&record.id);
|
let id_key = Self::record_id_key(&record.id);
|
||||||
|
if let Some(key) = self.get(&id_key).await? {
|
||||||
self.delete_key(&key).await?;
|
self.delete_key(&key).await?;
|
||||||
|
} else {
|
||||||
|
self.delete_key(&Self::record_key(
|
||||||
|
&record.zone_id,
|
||||||
|
&record.name,
|
||||||
|
record.record_type,
|
||||||
|
&record.id,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
self.delete_key(&Self::legacy_record_key(
|
||||||
|
&record.zone_id,
|
||||||
|
&record.name,
|
||||||
|
record.record_type,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
self.delete_key(&id_key).await?;
|
self.delete_key(&id_key).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -601,6 +658,7 @@ impl DnsMetadataStore {
|
||||||
a.name
|
a.name
|
||||||
.cmp(&b.name)
|
.cmp(&b.name)
|
||||||
.then(a.record_type.type_code().cmp(&b.record_type.type_code()))
|
.then(a.record_type.type_code().cmp(&b.record_type.type_code()))
|
||||||
|
.then(a.id.to_string().cmp(&b.id.to_string()))
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
|
|
@ -829,4 +887,38 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(deleted.is_none());
|
assert!(deleted.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_record_set_supports_multiple_records_with_same_name_and_type() {
|
||||||
|
let store = DnsMetadataStore::new_in_memory();
|
||||||
|
|
||||||
|
let zone_name = ZoneName::new("example.com").unwrap();
|
||||||
|
let zone = Zone::new(zone_name, "test-org", "test-project");
|
||||||
|
store.save_zone(&zone).await.unwrap();
|
||||||
|
|
||||||
|
let first = Record::new(
|
||||||
|
zone.id,
|
||||||
|
"api",
|
||||||
|
RecordData::a_from_str("192.168.1.10").unwrap(),
|
||||||
|
);
|
||||||
|
let second = Record::new(
|
||||||
|
zone.id,
|
||||||
|
"api",
|
||||||
|
RecordData::a_from_str("192.168.1.11").unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
store.save_record(&first).await.unwrap();
|
||||||
|
store.save_record(&second).await.unwrap();
|
||||||
|
|
||||||
|
let records = store.list_records_by_name(&zone.id, "api").await.unwrap();
|
||||||
|
assert_eq!(records.len(), 2);
|
||||||
|
assert!(records.iter().any(|record| record.id == first.id));
|
||||||
|
assert!(records.iter().any(|record| record.id == second.id));
|
||||||
|
|
||||||
|
store.delete_record(&first).await.unwrap();
|
||||||
|
|
||||||
|
let remaining = store.list_records_by_name(&zone.id, "api").await.unwrap();
|
||||||
|
assert_eq!(remaining.len(), 1);
|
||||||
|
assert_eq!(remaining[0].id, second.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,20 +231,20 @@ pub fn build_router(state: RestApiState) -> Router {
|
||||||
.route("/api/v1/auth/token", post(issue_token))
|
.route("/api/v1/auth/token", post(issue_token))
|
||||||
.route("/api/v1/auth/verify", post(verify_token))
|
.route("/api/v1/auth/verify", post(verify_token))
|
||||||
.route("/api/v1/users", get(list_users).post(create_user))
|
.route("/api/v1/users", get(list_users).post(create_user))
|
||||||
.route("/api/v1/users/:id", get(get_user))
|
.route("/api/v1/users/{id}", get(get_user))
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/orgs",
|
"/api/v1/orgs",
|
||||||
get(list_organizations).post(create_organization),
|
get(list_organizations).post(create_organization),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/orgs/:org_id",
|
"/api/v1/orgs/{org_id}",
|
||||||
get(get_organization)
|
get(get_organization)
|
||||||
.patch(update_organization)
|
.patch(update_organization)
|
||||||
.delete(delete_organization),
|
.delete(delete_organization),
|
||||||
)
|
)
|
||||||
.route("/api/v1/projects", get(list_projects).post(create_project))
|
.route("/api/v1/projects", get(list_projects).post(create_project))
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/orgs/:org_id/projects/:project_id",
|
"/api/v1/orgs/{org_id}/projects/{project_id}",
|
||||||
get(get_project)
|
get(get_project)
|
||||||
.patch(update_project)
|
.patch(update_project)
|
||||||
.delete(delete_project),
|
.delete(delete_project),
|
||||||
|
|
|
||||||
|
|
@ -902,6 +902,11 @@ impl QueryableStorage {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use axum::{
|
||||||
|
body::{to_bytes, Body},
|
||||||
|
http::{Method, Request, StatusCode},
|
||||||
|
};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_query_service_creation() {
|
async fn test_query_service_creation() {
|
||||||
|
|
@ -996,6 +1001,43 @@ mod tests {
|
||||||
assert!(values.contains(&"test_job".to_string()));
|
assert!(values.contains(&"test_job".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_label_values_route() {
|
||||||
|
let service = QueryService::new();
|
||||||
|
{
|
||||||
|
let mut storage = service.storage.write().await;
|
||||||
|
storage.upsert_series(TimeSeries {
|
||||||
|
id: SeriesId(1),
|
||||||
|
labels: vec![
|
||||||
|
Label::new("__name__", "test_metric"),
|
||||||
|
Label::new("job", "test_job"),
|
||||||
|
],
|
||||||
|
samples: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let app: axum::Router = service.router();
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri("/api/v1/label/job/values")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
let response = app
|
||||||
|
.oneshot(request)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
|
||||||
|
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(payload["status"], "success");
|
||||||
|
assert!(payload["data"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|value| value == "test_job"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_persistence_save_load_empty() {
|
fn test_persistence_save_load_empty() {
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ pub struct StorageStats {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use nightlight_types::Label;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_storage_creation() {
|
fn test_storage_creation() {
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,7 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
mode = mkOption {
|
mode = mkOption {
|
||||||
type = types.enum [ "load_balancer" "direct" ];
|
type = types.enum [ "load_balancer" "direct" "direct_multi" ];
|
||||||
default = "load_balancer";
|
default = "load_balancer";
|
||||||
description = "Whether DNS publishes the load balancer VIP or a direct instance address";
|
description = "Whether DNS publishes the load balancer VIP or a direct instance address";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
PhotonCloud local CI gates (provider-agnostic)
|
PhotonCloud local CI gates (provider-agnostic)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
photoncloud-gate [--tier 0|1|2] [--workspace <name>] [--shared-crates] [--no-logs] [--fix]
|
photoncloud-gate [--tier 0|1|2] [--workspace <name>] [--shared-crates] [--shared-crate <name>] [--no-logs] [--fix]
|
||||||
|
|
||||||
Tiers:
|
Tiers:
|
||||||
0: fmt + clippy + unit tests (lib) (fast, stable default)
|
0: fmt + clippy + unit tests (lib) (fast, stable default)
|
||||||
|
|
@ -79,6 +79,7 @@
|
||||||
tier="0"
|
tier="0"
|
||||||
only_ws=""
|
only_ws=""
|
||||||
shared_crates="0"
|
shared_crates="0"
|
||||||
|
only_shared_crate=""
|
||||||
no_logs="0"
|
no_logs="0"
|
||||||
fix="0"
|
fix="0"
|
||||||
|
|
||||||
|
|
@ -90,6 +91,8 @@
|
||||||
only_ws="$2"; shift 2;;
|
only_ws="$2"; shift 2;;
|
||||||
--shared-crates)
|
--shared-crates)
|
||||||
shared_crates="1"; shift 1;;
|
shared_crates="1"; shift 1;;
|
||||||
|
--shared-crate)
|
||||||
|
only_shared_crate="$2"; shift 2;;
|
||||||
--no-logs)
|
--no-logs)
|
||||||
no_logs="1"; shift 1;;
|
no_logs="1"; shift 1;;
|
||||||
--fix)
|
--fix)
|
||||||
|
|
@ -159,6 +162,8 @@
|
||||||
export LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
export LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
export PROTOC="${pkgs.protobuf}/bin/protoc"
|
export PROTOC="${pkgs.protobuf}/bin/protoc"
|
||||||
export ROCKSDB_LIB_DIR="${pkgs.rocksdb}/lib"
|
export ROCKSDB_LIB_DIR="${pkgs.rocksdb}/lib"
|
||||||
|
export CARGO_TARGET_DIR="$repo_root/work/cargo-target"
|
||||||
|
mkdir -p "$CARGO_TARGET_DIR"
|
||||||
|
|
||||||
manifest_has_target_kind() {
|
manifest_has_target_kind() {
|
||||||
local manifest="$1"; shift
|
local manifest="$1"; shift
|
||||||
|
|
@ -211,8 +216,46 @@
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run_shared_crate_clippy() {
|
||||||
|
local crate="$1"; shift
|
||||||
|
local manifest="$1"; shift
|
||||||
|
local ran_clippy="0"
|
||||||
|
|
||||||
|
if manifest_has_target_kind "$manifest" "lib"; then
|
||||||
|
run_shared_crate_cmd "$crate" "$manifest" "clippy (lib)" "$CARGO_CLIPPY clippy --manifest-path \"$manifest\" --lib -- -D warnings"
|
||||||
|
ran_clippy="1"
|
||||||
|
fi
|
||||||
|
if manifest_has_target_kind "$manifest" "bin"; then
|
||||||
|
run_shared_crate_cmd "$crate" "$manifest" "clippy (bin)" "$CARGO_CLIPPY clippy --manifest-path \"$manifest\" --bins -- -D warnings"
|
||||||
|
ran_clippy="1"
|
||||||
|
fi
|
||||||
|
if [[ "$ran_clippy" == "0" ]]; then
|
||||||
|
echo "[gate][shared:$crate] WARN: no lib/bin targets for clippy"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_workspace_clippy() {
|
||||||
|
local ws="$1"; shift
|
||||||
|
local manifest="$1"; shift
|
||||||
|
local ran_clippy="0"
|
||||||
|
|
||||||
|
if manifest_has_target_kind "$manifest" "lib"; then
|
||||||
|
run_cmd "$ws" "clippy (lib)" "$CARGO_CLIPPY clippy --workspace --lib -- -D warnings"
|
||||||
|
ran_clippy="1"
|
||||||
|
fi
|
||||||
|
if manifest_has_target_kind "$manifest" "bin"; then
|
||||||
|
run_cmd "$ws" "clippy (bin)" "$CARGO_CLIPPY clippy --workspace --bins -- -D warnings"
|
||||||
|
ran_clippy="1"
|
||||||
|
fi
|
||||||
|
if [[ "$ran_clippy" == "0" ]]; then
|
||||||
|
echo "[gate][$ws] WARN: no lib/bin targets for clippy"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
run_shared_crates() {
|
run_shared_crates() {
|
||||||
|
local only_crate="$1"
|
||||||
local manifests=()
|
local manifests=()
|
||||||
|
local selected_count=0
|
||||||
while IFS= read -r manifest; do
|
while IFS= read -r manifest; do
|
||||||
manifests+=("$manifest")
|
manifests+=("$manifest")
|
||||||
done < <(find "$repo_root/crates" -mindepth 2 -maxdepth 2 -name Cargo.toml | sort)
|
done < <(find "$repo_root/crates" -mindepth 2 -maxdepth 2 -name Cargo.toml | sort)
|
||||||
|
|
@ -226,8 +269,12 @@
|
||||||
local crate
|
local crate
|
||||||
local ran_unit_tests
|
local ran_unit_tests
|
||||||
crate="$(basename "$(dirname "$manifest")")"
|
crate="$(basename "$(dirname "$manifest")")"
|
||||||
|
if [[ -n "$only_crate" && "$crate" != "$only_crate" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
selected_count=$((selected_count + 1))
|
||||||
run_shared_crate_cmd "$crate" "$manifest" "fmt" "$CARGO_FMT fmt --manifest-path \"$manifest\" $fmt_rustfmt_args"
|
run_shared_crate_cmd "$crate" "$manifest" "fmt" "$CARGO_FMT fmt --manifest-path \"$manifest\" $fmt_rustfmt_args"
|
||||||
run_shared_crate_cmd "$crate" "$manifest" "clippy" "$CARGO_CLIPPY clippy --manifest-path \"$manifest\" --all-targets -- -D warnings"
|
run_shared_crate_clippy "$crate" "$manifest"
|
||||||
ran_unit_tests="0"
|
ran_unit_tests="0"
|
||||||
if manifest_has_target_kind "$manifest" "lib"; then
|
if manifest_has_target_kind "$manifest" "lib"; then
|
||||||
run_shared_crate_cmd "$crate" "$manifest" "test (tier0 unit lib)" "$CARGO test --manifest-path \"$manifest\" --lib"
|
run_shared_crate_cmd "$crate" "$manifest" "test (tier0 unit lib)" "$CARGO test --manifest-path \"$manifest\" --lib"
|
||||||
|
|
@ -249,10 +296,22 @@
|
||||||
run_shared_crate_cmd "$crate" "$manifest" "test (tier2 ignored)" "$CARGO test --manifest-path \"$manifest\" --tests -- --ignored"
|
run_shared_crate_cmd "$crate" "$manifest" "test (tier2 ignored)" "$CARGO test --manifest-path \"$manifest\" --tests -- --ignored"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ -n "$only_crate" && "$selected_count" -eq 0 ]]; then
|
||||||
|
echo "[gate] ERROR: shared crate not found: $only_crate" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "$shared_crates" == "1" ]]; then
|
if [[ "$shared_crates" == "1" ]]; then
|
||||||
run_shared_crates
|
run_shared_crates "$only_shared_crate"
|
||||||
|
echo ""
|
||||||
|
echo "[gate] OK (tier=$tier, shared-crates)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$only_shared_crate" ]]; then
|
||||||
|
run_shared_crates "$only_shared_crate"
|
||||||
echo ""
|
echo ""
|
||||||
echo "[gate] OK (tier=$tier, shared-crates)"
|
echo "[gate] OK (tier=$tier, shared-crates)"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -274,8 +333,9 @@
|
||||||
#
|
#
|
||||||
# NOTE: Avoid `--all` here; with path-dependencies it may traverse outside the workspace directory.
|
# NOTE: Avoid `--all` here; with path-dependencies it may traverse outside the workspace directory.
|
||||||
run_cmd "$ws" "fmt" "$CARGO_FMT fmt $fmt_rustfmt_args"
|
run_cmd "$ws" "fmt" "$CARGO_FMT fmt $fmt_rustfmt_args"
|
||||||
# Lint gate: call Nix-provided `cargo-clippy` directly (avoid resolving ~/.cargo/bin/cargo-clippy).
|
# Tier0 clippy stays on lib/bin targets to avoid dragging in
|
||||||
run_cmd "$ws" "clippy" "$CARGO_CLIPPY clippy --workspace --all-targets -- -D warnings"
|
# heavy integration-only dev-dependencies for every workspace.
|
||||||
|
run_workspace_clippy "$ws" "$workspace_manifest"
|
||||||
ran_unit_tests="0"
|
ran_unit_tests="0"
|
||||||
if manifest_has_target_kind "$workspace_manifest" "lib"; then
|
if manifest_has_target_kind "$workspace_manifest" "lib"; then
|
||||||
run_cmd "$ws" "test (tier0 unit lib)" "$CARGO test --workspace --lib"
|
run_cmd "$ws" "test (tier0 unit lib)" "$CARGO test --workspace --lib"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
".github/workflows/nix.yml",
|
".github/workflows/nix.yml",
|
||||||
"Cargo.toml",
|
"Cargo.toml",
|
||||||
"Cargo.lock",
|
"Cargo.lock",
|
||||||
"crates/**",
|
|
||||||
"baremetal/**",
|
"baremetal/**",
|
||||||
"scripts/**",
|
"scripts/**",
|
||||||
"specifications/**",
|
"specifications/**",
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,14 @@ in
|
||||||
timeoutSecs = 3;
|
timeoutSecs = 3;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
publish = {
|
||||||
|
dns = {
|
||||||
|
zone = "native.cluster.test";
|
||||||
|
name = "daemon";
|
||||||
|
ttl = 30;
|
||||||
|
mode = "direct_multi";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@
|
||||||
|
|
||||||
systemd.services.iam.environment = {
|
systemd.services.iam.environment = {
|
||||||
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
||||||
|
IAM_ALLOW_UNAUTHENTICATED_ADMIN = "true";
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.lightningstor.environment = {
|
systemd.services.lightningstor.environment = {
|
||||||
|
|
|
||||||
|
|
@ -68,5 +68,6 @@
|
||||||
|
|
||||||
systemd.services.iam.environment = {
|
systemd.services.iam.environment = {
|
||||||
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
||||||
|
IAM_ALLOW_UNAUTHENTICATED_ADMIN = "true";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,5 +68,6 @@
|
||||||
|
|
||||||
systemd.services.iam.environment = {
|
systemd.services.iam.environment = {
|
||||||
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
||||||
|
IAM_ALLOW_UNAUTHENTICATED_ADMIN = "true";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,15 @@ STORAGE_BENCHMARK_COMMAND="${PHOTON_VM_STORAGE_BENCH_COMMAND:-bench-storage}"
|
||||||
LIGHTNINGSTOR_BENCH_CLIENT_NODE="${PHOTON_VM_LIGHTNINGSTOR_BENCH_CLIENT_NODE:-node06}"
|
LIGHTNINGSTOR_BENCH_CLIENT_NODE="${PHOTON_VM_LIGHTNINGSTOR_BENCH_CLIENT_NODE:-node06}"
|
||||||
STORAGE_SKIP_PLASMAVMC_IMAGE_BENCH="${PHOTON_VM_SKIP_PLASMAVMC_IMAGE_BENCH:-0}"
|
STORAGE_SKIP_PLASMAVMC_IMAGE_BENCH="${PHOTON_VM_SKIP_PLASMAVMC_IMAGE_BENCH:-0}"
|
||||||
STORAGE_SKIP_PLASMAVMC_GUEST_RUNTIME_BENCH="${PHOTON_VM_SKIP_PLASMAVMC_GUEST_RUNTIME_BENCH:-0}"
|
STORAGE_SKIP_PLASMAVMC_GUEST_RUNTIME_BENCH="${PHOTON_VM_SKIP_PLASMAVMC_GUEST_RUNTIME_BENCH:-0}"
|
||||||
CLUSTER_NIX_MAX_JOBS="${PHOTON_CLUSTER_NIX_MAX_JOBS:-2}"
|
HOST_CPU_COUNT="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 4)"
|
||||||
CLUSTER_NIX_BUILD_CORES="${PHOTON_CLUSTER_NIX_BUILD_CORES:-4}"
|
DEFAULT_CLUSTER_NIX_MAX_JOBS=2
|
||||||
|
DEFAULT_CLUSTER_NIX_BUILD_CORES=4
|
||||||
|
if [[ "${HOST_CPU_COUNT}" =~ ^[0-9]+$ ]] && (( HOST_CPU_COUNT >= 12 )); then
|
||||||
|
DEFAULT_CLUSTER_NIX_MAX_JOBS=3
|
||||||
|
DEFAULT_CLUSTER_NIX_BUILD_CORES=6
|
||||||
|
fi
|
||||||
|
CLUSTER_NIX_MAX_JOBS="${PHOTON_CLUSTER_NIX_MAX_JOBS:-${DEFAULT_CLUSTER_NIX_MAX_JOBS}}"
|
||||||
|
CLUSTER_NIX_BUILD_CORES="${PHOTON_CLUSTER_NIX_BUILD_CORES:-${DEFAULT_CLUSTER_NIX_BUILD_CORES}}"
|
||||||
BUILD_PROFILE="${PHOTON_CLUSTER_BUILD_PROFILE:-default}"
|
BUILD_PROFILE="${PHOTON_CLUSTER_BUILD_PROFILE:-default}"
|
||||||
CLUSTER_SKIP_BUILD="${PHOTON_CLUSTER_SKIP_BUILD:-0}"
|
CLUSTER_SKIP_BUILD="${PHOTON_CLUSTER_SKIP_BUILD:-0}"
|
||||||
CLUSTER_LOCK_HELD=0
|
CLUSTER_LOCK_HELD=0
|
||||||
|
|
@ -4894,9 +4901,10 @@ validate_native_runtime_flow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
native_publication_state() {
|
native_publication_state() {
|
||||||
|
local service="$1"
|
||||||
native_dump_values "photoncloud/clusters/test-cluster/publications/" \
|
native_dump_values "photoncloud/clusters/test-cluster/publications/" \
|
||||||
| sed '/^$/d' \
|
| sed '/^$/d' \
|
||||||
| jq -sr 'map(select(.service == "native-web")) | first'
|
| jq -sr --arg service "${service}" 'map(select(.service == $service)) | first'
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_native_dns_record() {
|
wait_for_native_dns_record() {
|
||||||
|
|
@ -4916,6 +4924,33 @@ validate_native_runtime_flow() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wait_for_native_dns_records() {
|
||||||
|
local fqdn="$1"
|
||||||
|
local timeout="$2"
|
||||||
|
shift 2
|
||||||
|
local expected_json actual_json
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
|
expected_json="$(printf '%s\n' "$@" | sed '/^$/d' | sort -u | jq -R . | jq -cs 'sort')"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
actual_json="$(
|
||||||
|
ssh_node node01 "dig @127.0.0.1 -p 5353 +short ${fqdn} A" 2>/dev/null \
|
||||||
|
| sed '/^$/d' \
|
||||||
|
| sort -u \
|
||||||
|
| jq -R . \
|
||||||
|
| jq -cs 'sort'
|
||||||
|
)" || actual_json="[]"
|
||||||
|
if [[ "${actual_json}" == "${expected_json}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if (( SECONDS >= deadline )); then
|
||||||
|
die "timed out waiting for native DNS record set for ${fqdn}: expected ${expected_json}, got ${actual_json}"
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_native_lb_backends() {
|
wait_for_native_lb_backends() {
|
||||||
local pool_id="$1"
|
local pool_id="$1"
|
||||||
local expected_count="$2"
|
local expected_count="$2"
|
||||||
|
|
@ -5023,8 +5058,8 @@ validate_native_runtime_flow() {
|
||||||
|
|
||||||
wait_for_native_dump_count \
|
wait_for_native_dump_count \
|
||||||
"photoncloud/clusters/test-cluster/publications/" \
|
"photoncloud/clusters/test-cluster/publications/" \
|
||||||
'map(select(.service == "native-web")) | length' \
|
'map(select(.service == "native-web" or .service == "native-daemon")) | length' \
|
||||||
"1" \
|
"2" \
|
||||||
180
|
180
|
||||||
|
|
||||||
iam_tunnel="$(start_ssh_tunnel node01 15080 50080)"
|
iam_tunnel="$(start_ssh_tunnel node01 15080 50080)"
|
||||||
|
|
@ -5041,15 +5076,20 @@ validate_native_runtime_flow() {
|
||||||
| jq -e --arg name "${lb_name}" '.loadbalancers | any(.name == $name)' >/dev/null
|
| jq -e --arg name "${lb_name}" '.loadbalancers | any(.name == $name)' >/dev/null
|
||||||
|
|
||||||
local publication_value publication_fqdn publication_ip publication_pool_id
|
local publication_value publication_fqdn publication_ip publication_pool_id
|
||||||
publication_value="$(native_publication_state)"
|
local daemon_publication_value daemon_publication_fqdn
|
||||||
|
publication_value="$(native_publication_state "native-web")"
|
||||||
publication_fqdn="$(printf '%s' "${publication_value}" | jq -r '.dns.fqdn')"
|
publication_fqdn="$(printf '%s' "${publication_value}" | jq -r '.dns.fqdn')"
|
||||||
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
||||||
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
||||||
[[ -n "${publication_fqdn}" && "${publication_fqdn}" != "null" ]] || die "native publication missing fqdn"
|
daemon_publication_value="$(native_publication_state "native-daemon")"
|
||||||
[[ -n "${publication_ip}" && "${publication_ip}" != "null" ]] || die "native publication missing dns value"
|
daemon_publication_fqdn="$(printf '%s' "${daemon_publication_value}" | jq -r '.dns.fqdn')"
|
||||||
[[ -n "${publication_pool_id}" && "${publication_pool_id}" != "null" ]] || die "native publication missing pool id"
|
[[ -n "${publication_fqdn}" && "${publication_fqdn}" != "null" ]] || die "native-web publication missing fqdn"
|
||||||
|
[[ -n "${publication_ip}" && "${publication_ip}" != "null" ]] || die "native-web publication missing dns value"
|
||||||
|
[[ -n "${publication_pool_id}" && "${publication_pool_id}" != "null" ]] || die "native-web publication missing pool id"
|
||||||
|
[[ -n "${daemon_publication_fqdn}" && "${daemon_publication_fqdn}" != "null" ]] || die "native-daemon publication missing fqdn"
|
||||||
|
|
||||||
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
||||||
|
wait_for_native_dns_records "${daemon_publication_fqdn}" 180 10.100.0.21 10.100.0.22
|
||||||
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
|
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
|
||||||
|
|
||||||
log "Draining node04 through deployer lifecycle state"
|
log "Draining node04 through deployer lifecycle state"
|
||||||
|
|
@ -5101,11 +5141,13 @@ validate_native_runtime_flow() {
|
||||||
wait_for_http node05 "http://10.100.0.22:18192/" 240
|
wait_for_http node05 "http://10.100.0.22:18192/" 240
|
||||||
wait_for_http node05 "http://10.100.0.22:18193/" 240
|
wait_for_http node05 "http://10.100.0.22:18193/" 240
|
||||||
wait_for_http node01 "http://127.0.0.1:18191/" 240
|
wait_for_http node01 "http://127.0.0.1:18191/" 240
|
||||||
publication_value="$(native_publication_state)"
|
publication_value="$(native_publication_state "native-web")"
|
||||||
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
||||||
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
||||||
|
daemon_publication_value="$(native_publication_state "native-daemon")"
|
||||||
wait_for_native_lb_backends "${publication_pool_id}" "1" 180 10.100.0.22
|
wait_for_native_lb_backends "${publication_pool_id}" "1" 180 10.100.0.22
|
||||||
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
||||||
|
wait_for_native_dns_records "${daemon_publication_fqdn}" 180 10.100.0.22
|
||||||
|
|
||||||
log "Restoring node04 and ensuring capacity returns without moving healthy singleton work"
|
log "Restoring node04 and ensuring capacity returns without moving healthy singleton work"
|
||||||
set_native_node_state "node04" "active"
|
set_native_node_state "node04" "active"
|
||||||
|
|
@ -5147,11 +5189,13 @@ validate_native_runtime_flow() {
|
||||||
restored_container_value="$(wait_for_native_instance_node "native-container" "node05" 240)"
|
restored_container_value="$(wait_for_native_instance_node "native-container" "node05" 240)"
|
||||||
restored_container_node="$(printf '%s' "${restored_container_value}" | jq -r '.node_id')"
|
restored_container_node="$(printf '%s' "${restored_container_value}" | jq -r '.node_id')"
|
||||||
[[ "${restored_container_node}" == "node05" ]] || die "native-container unexpectedly moved after node04 returned to service"
|
[[ "${restored_container_node}" == "node05" ]] || die "native-container unexpectedly moved after node04 returned to service"
|
||||||
publication_value="$(native_publication_state)"
|
publication_value="$(native_publication_state "native-web")"
|
||||||
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
||||||
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
||||||
|
daemon_publication_value="$(native_publication_state "native-daemon")"
|
||||||
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
|
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
|
||||||
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
||||||
|
wait_for_native_dns_records "${daemon_publication_fqdn}" 180 10.100.0.21 10.100.0.22
|
||||||
wait_for_http node01 "http://127.0.0.1:18191/" 240
|
wait_for_http node01 "http://127.0.0.1:18191/" 240
|
||||||
|
|
||||||
log "Simulating native worker loss and scheduler failover"
|
log "Simulating native worker loss and scheduler failover"
|
||||||
|
|
@ -5182,11 +5226,13 @@ validate_native_runtime_flow() {
|
||||||
failover_container_node="$(printf '%s' "${failover_container_value}" | jq -r '.node_id')"
|
failover_container_node="$(printf '%s' "${failover_container_value}" | jq -r '.node_id')"
|
||||||
[[ "${failover_container_node}" == "node04" ]] || die "native-container did not fail over to node04 after node05 stopped"
|
[[ "${failover_container_node}" == "node04" ]] || die "native-container did not fail over to node04 after node05 stopped"
|
||||||
wait_for_native_instance_node "native-daemon" "node04" 240 >/dev/null
|
wait_for_native_instance_node "native-daemon" "node04" 240 >/dev/null
|
||||||
publication_value="$(native_publication_state)"
|
publication_value="$(native_publication_state "native-web")"
|
||||||
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
||||||
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
||||||
|
daemon_publication_value="$(native_publication_state "native-daemon")"
|
||||||
wait_for_native_lb_backends "${publication_pool_id}" "1" 240 10.100.0.21
|
wait_for_native_lb_backends "${publication_pool_id}" "1" 240 10.100.0.21
|
||||||
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
||||||
|
wait_for_native_dns_records "${daemon_publication_fqdn}" 180 10.100.0.21
|
||||||
wait_for_http node04 "http://10.100.0.21:18190/" 240
|
wait_for_http node04 "http://10.100.0.21:18190/" 240
|
||||||
wait_for_http node04 "http://10.100.0.21:18192/" 240
|
wait_for_http node04 "http://10.100.0.21:18192/" 240
|
||||||
wait_for_http node04 "http://10.100.0.21:18193/" 240
|
wait_for_http node04 "http://10.100.0.21:18193/" 240
|
||||||
|
|
@ -5236,11 +5282,13 @@ validate_native_runtime_flow() {
|
||||||
recovered_container_value="$(wait_for_native_instance_node "native-container" "node04" 240)"
|
recovered_container_value="$(wait_for_native_instance_node "native-container" "node04" 240)"
|
||||||
recovered_container_node="$(printf '%s' "${recovered_container_value}" | jq -r '.node_id')"
|
recovered_container_node="$(printf '%s' "${recovered_container_value}" | jq -r '.node_id')"
|
||||||
[[ "${recovered_container_node}" == "node04" ]] || die "native-container unexpectedly churned after node05 recovered"
|
[[ "${recovered_container_node}" == "node04" ]] || die "native-container unexpectedly churned after node05 recovered"
|
||||||
publication_value="$(native_publication_state)"
|
publication_value="$(native_publication_state "native-web")"
|
||||||
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
publication_pool_id="$(printf '%s' "${publication_value}" | jq -r '.load_balancer.pool_id')"
|
||||||
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
publication_ip="$(printf '%s' "${publication_value}" | jq -r '.dns.value')"
|
||||||
|
daemon_publication_value="$(native_publication_state "native-daemon")"
|
||||||
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
|
wait_for_native_lb_backends "${publication_pool_id}" "2" 180 10.100.0.21 10.100.0.22
|
||||||
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
wait_for_native_dns_record "${publication_fqdn}" "${publication_ip}" 180
|
||||||
|
wait_for_native_dns_records "${daemon_publication_fqdn}" 180 10.100.0.21 10.100.0.22
|
||||||
wait_for_http node01 "http://127.0.0.1:18191/" 240
|
wait_for_http node01 "http://127.0.0.1:18191/" 240
|
||||||
|
|
||||||
trap - RETURN
|
trap - RETURN
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@
|
||||||
|
|
||||||
systemd.services.iam.environment = {
|
systemd.services.iam.environment = {
|
||||||
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
||||||
|
IAM_ALLOW_UNAUTHENTICATED_ADMIN = "true";
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.lightningstor.environment = {
|
systemd.services.lightningstor.environment = {
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,6 @@
|
||||||
|
|
||||||
systemd.services.iam.environment = {
|
systemd.services.iam.environment = {
|
||||||
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
||||||
|
IAM_ALLOW_UNAUTHENTICATED_ADMIN = "true";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,6 @@
|
||||||
|
|
||||||
systemd.services.iam.environment = {
|
systemd.services.iam.environment = {
|
||||||
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
IAM_ALLOW_RANDOM_SIGNING_KEY = "1";
|
||||||
|
IAM_ALLOW_UNAUTHENTICATED_ADMIN = "true";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import argparse
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,26 +25,149 @@ def matches_any(path: str, patterns: list[str]) -> bool:
|
||||||
return any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
|
return any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
|
||||||
|
|
||||||
|
|
||||||
def detect_changes(config: dict[str, Any], changed_files: list[str]) -> dict[str, Any]:
|
def workspace_root(workspace: dict[str, Any]) -> str:
|
||||||
|
for pattern in workspace["paths"]:
|
||||||
|
root = pattern.split("/", 1)[0]
|
||||||
|
if root and "*" not in root and "?" not in root:
|
||||||
|
return root
|
||||||
|
raise ValueError(f"Could not infer workspace root for {workspace['name']}")
|
||||||
|
|
||||||
|
|
||||||
|
def collect_path_dependencies(obj: Any) -> list[str]:
|
||||||
|
path_dependencies: list[str] = []
|
||||||
|
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
path = obj.get("path")
|
||||||
|
if isinstance(path, str):
|
||||||
|
path_dependencies.append(path)
|
||||||
|
for value in obj.values():
|
||||||
|
path_dependencies.extend(collect_path_dependencies(value))
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for value in obj:
|
||||||
|
path_dependencies.extend(collect_path_dependencies(value))
|
||||||
|
|
||||||
|
return path_dependencies
|
||||||
|
|
||||||
|
|
||||||
|
def build_nodes(config: dict[str, Any], repo_root: Path) -> dict[str, dict[str, Any]]:
|
||||||
|
nodes: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
for workspace in config["workspaces"]:
|
||||||
|
nodes[workspace["name"]] = {
|
||||||
|
"name": workspace["name"],
|
||||||
|
"kind": "workspace",
|
||||||
|
"root": repo_root / workspace_root(workspace),
|
||||||
|
}
|
||||||
|
|
||||||
|
crates_dir = repo_root / "crates"
|
||||||
|
if crates_dir.is_dir():
|
||||||
|
for manifest in sorted(crates_dir.glob("*/Cargo.toml")):
|
||||||
|
crate_name = manifest.parent.name
|
||||||
|
nodes[f"crate:{crate_name}"] = {
|
||||||
|
"name": crate_name,
|
||||||
|
"kind": "shared_crate",
|
||||||
|
"root": manifest.parent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_node_for_path(path: Path, nodes: dict[str, dict[str, Any]]) -> str | None:
|
||||||
|
for node_name, node in sorted(
|
||||||
|
nodes.items(),
|
||||||
|
key=lambda item: len(str(item[1]["root"])),
|
||||||
|
reverse=True,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
path.relative_to(node["root"])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return node_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_dependency_graph(
|
||||||
|
nodes: dict[str, dict[str, Any]],
|
||||||
|
) -> dict[str, set[str]]:
|
||||||
|
graph = {node_name: set() for node_name in nodes}
|
||||||
|
|
||||||
|
for node_name, node in nodes.items():
|
||||||
|
root = node["root"]
|
||||||
|
for manifest in root.rglob("Cargo.toml"):
|
||||||
|
manifest_data = tomllib.loads(manifest.read_text())
|
||||||
|
for dependency_path in collect_path_dependencies(manifest_data):
|
||||||
|
resolved_dependency = (manifest.parent / dependency_path).resolve()
|
||||||
|
dependency_node = resolve_node_for_path(resolved_dependency, nodes)
|
||||||
|
if dependency_node is None or dependency_node == node_name:
|
||||||
|
continue
|
||||||
|
graph[node_name].add(dependency_node)
|
||||||
|
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_graph(graph: dict[str, set[str]]) -> dict[str, set[str]]:
|
||||||
|
reversed_graph = {node_name: set() for node_name in graph}
|
||||||
|
for node_name, dependencies in graph.items():
|
||||||
|
for dependency in dependencies:
|
||||||
|
reversed_graph[dependency].add(node_name)
|
||||||
|
return reversed_graph
|
||||||
|
|
||||||
|
|
||||||
|
def impacted_nodes(
|
||||||
|
changed_nodes: set[str],
|
||||||
|
reversed_dependencies: dict[str, set[str]],
|
||||||
|
) -> set[str]:
|
||||||
|
selected = set(changed_nodes)
|
||||||
|
queue = list(changed_nodes)
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current = queue.pop()
|
||||||
|
for dependent in reversed_dependencies[current]:
|
||||||
|
if dependent in selected:
|
||||||
|
continue
|
||||||
|
selected.add(dependent)
|
||||||
|
queue.append(dependent)
|
||||||
|
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def detect_changes(
|
||||||
|
config: dict[str, Any],
|
||||||
|
changed_files: list[str],
|
||||||
|
repo_root: Path,
|
||||||
|
) -> dict[str, Any]:
|
||||||
workspaces: list[dict[str, Any]] = config["workspaces"]
|
workspaces: list[dict[str, Any]] = config["workspaces"]
|
||||||
all_workspace_names = [workspace["name"] for workspace in workspaces]
|
all_workspace_names = [workspace["name"] for workspace in workspaces]
|
||||||
|
nodes = build_nodes(config, repo_root)
|
||||||
|
dependency_graph = build_dependency_graph(nodes)
|
||||||
|
reversed_dependencies = reverse_graph(dependency_graph)
|
||||||
|
|
||||||
global_changed = any(
|
global_changed = any(
|
||||||
matches_any(path, config["global_paths"])
|
matches_any(path, config["global_paths"])
|
||||||
for path in changed_files
|
for path in changed_files
|
||||||
)
|
)
|
||||||
shared_crates_changed = any(
|
directly_changed_nodes = {
|
||||||
matches_any(path, config["shared_crates_paths"])
|
node_name
|
||||||
for path in changed_files
|
for path in changed_files
|
||||||
|
for node_name in [resolve_node_for_path((repo_root / path).resolve(), nodes)]
|
||||||
|
if node_name is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_crates = sorted(
|
||||||
|
nodes[node_name]["name"]
|
||||||
|
for node_name in directly_changed_nodes
|
||||||
|
if nodes[node_name]["kind"] == "shared_crate"
|
||||||
)
|
)
|
||||||
|
shared_crates_changed = bool(shared_crates)
|
||||||
|
|
||||||
if global_changed:
|
if global_changed:
|
||||||
changed_workspaces = all_workspace_names
|
changed_workspaces = all_workspace_names
|
||||||
else:
|
else:
|
||||||
|
selected_nodes = impacted_nodes(directly_changed_nodes, reversed_dependencies)
|
||||||
changed_workspaces = [
|
changed_workspaces = [
|
||||||
workspace["name"]
|
workspace["name"]
|
||||||
for workspace in workspaces
|
for workspace in workspaces
|
||||||
if any(matches_any(path, workspace["paths"]) for path in changed_files)
|
if workspace["name"] in selected_nodes
|
||||||
]
|
]
|
||||||
|
|
||||||
selected_workspaces = set(changed_workspaces)
|
selected_workspaces = set(changed_workspaces)
|
||||||
|
|
@ -70,6 +194,7 @@ def detect_changes(config: dict[str, Any], changed_files: list[str]) -> dict[str
|
||||||
"any_changed": global_changed or bool(changed_workspaces),
|
"any_changed": global_changed or bool(changed_workspaces),
|
||||||
"build_changed": bool(build_targets),
|
"build_changed": bool(build_targets),
|
||||||
"global_changed": global_changed,
|
"global_changed": global_changed,
|
||||||
|
"shared_crates": shared_crates,
|
||||||
"shared_crates_changed": shared_crates_changed,
|
"shared_crates_changed": shared_crates_changed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +206,7 @@ def write_github_output(path: Path, result: dict[str, Any]) -> None:
|
||||||
"any_changed": str(result["any_changed"]).lower(),
|
"any_changed": str(result["any_changed"]).lower(),
|
||||||
"build_changed": str(result["build_changed"]).lower(),
|
"build_changed": str(result["build_changed"]).lower(),
|
||||||
"global_changed": str(result["global_changed"]).lower(),
|
"global_changed": str(result["global_changed"]).lower(),
|
||||||
|
"shared_crates": json.dumps(result["shared_crates"], separators=(",", ":")),
|
||||||
"shared_crates_changed": str(result["shared_crates_changed"]).lower(),
|
"shared_crates_changed": str(result["shared_crates_changed"]).lower(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,9 +245,11 @@ def parse_args() -> argparse.Namespace:
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
config = json.loads(Path(args.config).read_text())
|
config_path = Path(args.config).resolve()
|
||||||
|
repo_root = config_path.parents[2]
|
||||||
|
config = json.loads(config_path.read_text())
|
||||||
changed_files = load_changed_files(args)
|
changed_files = load_changed_files(args)
|
||||||
result = detect_changes(config, changed_files)
|
result = detect_changes(config, changed_files, repo_root)
|
||||||
|
|
||||||
if args.github_output:
|
if args.github_output:
|
||||||
write_github_output(Path(args.github_output), result)
|
write_github_output(Path(args.github_output), result)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue