refactor(fleet-operator): replace ScorePayload with ReconcileScore in Deployment CRD [NationTech/Team#186] #278
@@ -29,7 +29,8 @@ use async_nats::jetstream::{self, kv};
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use harmony_fleet_operator::crd::{
|
||||
Deployment, DeploymentSpec, Rollout, RolloutStrategy, ScorePayload,
|
||||
Deployment, DeploymentSpec, PodmanService, PodmanV0Score, ReconcileScore, Rollout,
|
||||
RolloutStrategy,
|
||||
};
|
||||
use harmony_reconciler_contracts::{
|
||||
BUCKET_DEVICE_HEARTBEAT, BUCKET_DEVICE_INFO, BUCKET_DEVICE_STATE, DeploymentName,
|
||||
@@ -415,16 +416,16 @@ async fn apply_one_cr(
|
||||
// the desired-state here. The aggregator still writes KV
|
||||
// for each matched device; that's wire noise we accept
|
||||
// as part of the realism.
|
||||
score: ScorePayload {
|
||||
type_: "PodmanV0".to_string(),
|
||||
data: serde_json::json!({
|
||||
"services": [{
|
||||
"name": group.cr_name,
|
||||
"image": "docker.io/library/nginx:alpine",
|
||||
"ports": ["8080:80"],
|
||||
}],
|
||||
}),
|
||||
},
|
||||
score: ReconcileScore::PodmanV0(PodmanV0Score {
|
||||
services: vec![PodmanService {
|
||||
name: group.cr_name.clone(),
|
||||
image: "docker.io/library/nginx:alpine".to_string(),
|
||||
ports: vec!["8080:80".to_string()],
|
||||
env: vec![],
|
||||
volumes: vec![],
|
||||
restart_policy: Default::default(),
|
||||
}],
|
||||
}),
|
||||
rollout: Rollout {
|
||||
strategy: RolloutStrategy::Immediate,
|
||||
},
|
||||
|
||||
@@ -38,11 +38,9 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use harmony::modules::podman::{PodmanService, PodmanV0Score};
|
||||
use harmony::modules::podman::{PodmanService, PodmanV0Score, ReconcileScore};
|
||||
use harmony::topology::{RestartPolicy, VolumeMount};
|
||||
use harmony_fleet_operator::crd::{
|
||||
Deployment, DeploymentSpec, Rollout, RolloutStrategy, ScorePayload,
|
||||
};
|
||||
use harmony_fleet_operator::crd::{Deployment, DeploymentSpec, Rollout, RolloutStrategy};
|
||||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
|
||||
use kube::Client;
|
||||
use kube::api::{Api, DeleteParams, Patch, PatchParams};
|
||||
@@ -209,14 +207,7 @@ fn build_cr(cli: &Cli) -> Deployment {
|
||||
}],
|
||||
};
|
||||
|
||||
let payload = ScorePayload {
|
||||
type_: "PodmanV0".to_string(),
|
||||
// `ScorePayload::data` is `serde_json::Value` by design
|
||||
// (opaque payload routed to the agent). Serialize the typed
|
||||
// score through serde_json — the agent's `ReconcileScore` enum
|
||||
// accepts exactly this shape via `#[serde(tag, content)]`.
|
||||
data: serde_json::to_value(&score).expect("PodmanV0Score is JSON-clean"),
|
||||
};
|
||||
let payload = ReconcileScore::PodmanV0(score);
|
||||
|
||||
let mut match_labels = BTreeMap::new();
|
||||
if cli.selectors.is_empty() {
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
rust-version = "1.85"
|
||||
|
||||
[dependencies]
|
||||
harmony = { path = "../../harmony" }
|
||||
harmony = { path = "../../harmony", features = ["podman"] }
|
||||
harmony-fleet-auth = { path = "../harmony-fleet-auth" }
|
||||
harmony-reconciler-contracts = { path = "../../harmony-reconciler-contracts" }
|
||||
toml = { workspace = true }
|
||||
@@ -22,4 +22,4 @@ tracing-subscriber = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
futures-util = { workspace = true }
|
||||
thiserror.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -2,11 +2,10 @@ use harmony_reconciler_contracts::InventorySnapshot;
|
||||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
|
||||
use kube::CustomResource;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::schema::{
|
||||
InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, StringValidation,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use harmony::modules::podman::{PodmanService, PodmanV0Score, ReconcileScore};
|
||||
|
||||
/// Deployment intent. Targets devices by label selector — identical
|
||||
/// to the pattern K8s itself uses for DaemonSet nodeSelector, Service
|
||||
/// pod selector, etc. The operator resolves the selector against
|
||||
@@ -26,73 +25,10 @@ pub struct DeploymentSpec {
|
||||
/// Which devices this deployment targets. matches against
|
||||
/// `Device.metadata.labels`.
|
||||
pub target_selector: LabelSelector,
|
||||
#[schemars(schema_with = "score_payload_schema")]
|
||||
pub score: ScorePayload,
|
||||
pub score: ReconcileScore,
|
||||
pub rollout: Rollout,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
|
||||
pub struct ScorePayload {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Hand-rolled schema for `ScorePayload` so we can attach two apiserver
|
||||
/// concessions that `schemars` can't derive:
|
||||
///
|
||||
/// 1. `x-kubernetes-preserve-unknown-fields: true` on `data` — the payload
|
||||
/// is routed opaquely; its shape is enforced on-device by the agent's
|
||||
/// typed `ReconcileScore` deserialization, not by the apiserver.
|
||||
/// 2. An `x-kubernetes-validations` CEL rule on the enclosing `score` object
|
||||
/// requiring `type` to be a valid Rust identifier, so typos (`"pdoman"`)
|
||||
/// are rejected at `kubectl apply` time rather than silently reaching
|
||||
/// the agent.
|
||||
fn score_payload_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
let type_schema = Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
string: Some(Box::new(StringValidation {
|
||||
min_length: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut data_schema = SchemaObject::default();
|
||||
data_schema.extensions.insert(
|
||||
"x-kubernetes-preserve-unknown-fields".to_string(),
|
||||
serde_json::Value::Bool(true),
|
||||
);
|
||||
|
||||
let object = ObjectValidation {
|
||||
required: ["type".to_string(), "data".to_string()]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
properties: [
|
||||
("type".to_string(), type_schema),
|
||||
("data".to_string(), Schema::Object(data_schema)),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut obj = SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
|
||||
object: Some(Box::new(object)),
|
||||
..Default::default()
|
||||
};
|
||||
obj.extensions.insert(
|
||||
"x-kubernetes-validations".to_string(),
|
||||
serde_json::json!([{
|
||||
"rule": "self.type.matches('^[A-Za-z_][A-Za-z0-9_]*$')",
|
||||
"message": "score.type must be a valid Rust identifier matching the struct name of the score variant (e.g. PodmanV0)"
|
||||
}]),
|
||||
);
|
||||
|
||||
Schema::Object(obj)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
|
||||
pub struct Rollout {
|
||||
pub strategy: RolloutStrategy,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! The CRD type definitions are exposed here as a library so external
|
||||
//! consumers — tooling that applies CRs, tests, documentation generators
|
||||
//! — can import the typed `Deployment`, `DeploymentSpec`,
|
||||
//! `ScorePayload`, etc. without duplicating them.
|
||||
//! `ReconcileScore`, etc. without duplicating them.
|
||||
|
||||
pub mod chart;
|
||||
pub mod crd;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::executors::ExecutorError;
|
||||
@@ -76,7 +77,7 @@ impl ContainerSpec {
|
||||
/// A single host-path → container-path bind mount. Bind mounts are the only
|
||||
/// volume kind supported in v0 — they cover ~95% of compose use cases and
|
||||
/// don't depend on a runtime-managed volume namespace.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, JsonSchema, Eq, Serialize, Deserialize)]
|
||||
pub struct VolumeMount {
|
||||
/// Absolute path on the host.
|
||||
pub host_path: String,
|
||||
@@ -90,7 +91,7 @@ pub struct VolumeMount {
|
||||
|
||||
/// Restart policy for a managed container. Names follow podman/docker
|
||||
/// conventions so docker-compose translation is mechanical.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, JsonSchema, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum RestartPolicy {
|
||||
/// Don't restart on exit.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! independent of any particular product (IoT fleet, OKD fleet, etc.)
|
||||
//! — callers serialize them over whatever transport they like.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@@ -22,7 +23,7 @@ use super::interpret::PodmanV0Interpret;
|
||||
/// Wire-compatible with prior releases: the new `env`, `volumes`, and
|
||||
/// `restart_policy` fields all default to empty / `unless-stopped` so older
|
||||
/// Deployment CRs without them deserialize unchanged.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct PodmanService {
|
||||
pub name: String,
|
||||
pub image: String,
|
||||
@@ -42,7 +43,7 @@ pub struct PodmanService {
|
||||
}
|
||||
|
||||
/// v0 Score for podman-based workloads.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct PodmanV0Score {
|
||||
pub services: Vec<PodmanService>,
|
||||
}
|
||||
@@ -69,7 +70,7 @@ impl PodmanV0Score {
|
||||
/// Adding a new variant is additive — emitters stay opaque routers,
|
||||
/// consumers learn the new variant, older consumers harmlessly
|
||||
/// log-and-skip the unknown tag.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
pub enum ReconcileScore {
|
||||
PodmanV0(PodmanV0Score),
|
||||
|
||||
Reference in New Issue
Block a user