Compare commits
1 Commits
master
...
worktree-b
| Author | SHA1 | Date | |
|---|---|---|---|
| 82d1f87ff8 |
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -3010,13 +3010,6 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "harmony_i18n"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "harmony_inventory_agent"
|
name = "harmony_inventory_agent"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ members = [
|
|||||||
"harmony_inventory_agent",
|
"harmony_inventory_agent",
|
||||||
"harmony_secret_derive",
|
"harmony_secret_derive",
|
||||||
"harmony_secret",
|
"harmony_secret",
|
||||||
"harmony_i18n",
|
|
||||||
"harmony_config_derive",
|
"harmony_config_derive",
|
||||||
"harmony_config",
|
"harmony_config",
|
||||||
"brocade",
|
"brocade",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "example-monitoring".to_string(),
|
name: "example-monitoring".to_string(),
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
dns: "example-monitoring.harmony.mcd".to_string(),
|
dns: "example-monitoring.harmony.mcd".to_string(),
|
||||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use harmony_types::{k8s_name::K8sName, net::Url};
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "test-rhob-monitoring".to_string(),
|
name: "test-rhob-monitoring".to_string(),
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
dns: "test-rhob-monitoring.harmony.mcd".to_string(),
|
dns: "test-rhob-monitoring.harmony.mcd".to_string(),
|
||||||
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use harmony_types::k8s_name::K8sName;
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "harmony-example-rust-webapp".to_string(),
|
name: "harmony-example-rust-webapp".to_string(),
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
dns: "harmony-example-rust-webapp.harmony.mcd".to_string(),
|
dns: "harmony-example-rust-webapp.harmony.mcd".to_string(),
|
||||||
project_root: PathBuf::from("./webapp"),
|
project_root: PathBuf::from("./webapp"),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use std::{path::PathBuf, sync::Arc};
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
let application = Arc::new(RustWebapp {
|
let application = Arc::new(RustWebapp {
|
||||||
name: "harmony-example-tryrust".to_string(),
|
name: "harmony-example-tryrust".to_string(),
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
dns: "tryrust.example.harmony.mcd".to_string(),
|
dns: "tryrust.example.harmony.mcd".to_string(),
|
||||||
project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
|
project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
|
||||||
// submodule
|
// submodule
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use kube::{Error, Resource, ResourceExt, api::DynamicObject};
|
use kube::{Error, Resource, ResourceExt, api::DynamicObject, core::ErrorResponse};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
@@ -117,16 +117,13 @@ impl ResourceBundle {
|
|||||||
/// Delete all resources in this bundle from the cluster.
|
/// Delete all resources in this bundle from the cluster.
|
||||||
/// Resources are deleted in reverse order to respect dependencies.
|
/// Resources are deleted in reverse order to respect dependencies.
|
||||||
pub async fn delete(&self, client: &K8sClient) -> Result<(), Error> {
|
pub async fn delete(&self, client: &K8sClient) -> Result<(), Error> {
|
||||||
// FIXME delete all in parallel and retry using kube::client::retry::RetryPolicy
|
|
||||||
for res in self.resources.iter().rev() {
|
for res in self.resources.iter().rev() {
|
||||||
let api = client.get_api_for_dynamic_object(res, res.namespace().as_deref())?;
|
let api = client.get_api_for_dynamic_object(res, res.namespace().as_deref())?;
|
||||||
let name = res.name_any();
|
let name = res.name_any();
|
||||||
// FIXME this swallows all errors. Swallowing a 404 is ok but other errors must be
|
match api.delete(&name, &kube::api::DeleteParams::default()).await {
|
||||||
// handled properly (such as retrying). A normal error case is when we delete a
|
Ok(_) | Err(Error::Api(ErrorResponse { code: 404, .. })) => {}
|
||||||
// resource bundle with dependencies between various resources. Such as a pod with a
|
Err(e) => return Err(e),
|
||||||
// dependency on a ClusterRoleBinding. Trying to delete the ClusterRoleBinding first
|
}
|
||||||
// is expected to fail
|
|
||||||
let _ = api.delete(&name, &kube::api::DeleteParams::default()).await;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ pub enum InterpretName {
|
|||||||
K8sPrometheusCrdAlerting,
|
K8sPrometheusCrdAlerting,
|
||||||
CephRemoveOsd,
|
CephRemoveOsd,
|
||||||
DiscoverInventoryAgent,
|
DiscoverInventoryAgent,
|
||||||
DeployInventoryAgent,
|
|
||||||
CephClusterHealth,
|
CephClusterHealth,
|
||||||
Custom(&'static str),
|
Custom(&'static str),
|
||||||
RHOBAlerting,
|
RHOBAlerting,
|
||||||
@@ -65,7 +64,6 @@ impl std::fmt::Display for InterpretName {
|
|||||||
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
|
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
|
||||||
InterpretName::CephRemoveOsd => f.write_str("CephRemoveOsd"),
|
InterpretName::CephRemoveOsd => f.write_str("CephRemoveOsd"),
|
||||||
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
||||||
InterpretName::DeployInventoryAgent => f.write_str("DeployInventoryAgent"),
|
|
||||||
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
|
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
|
||||||
InterpretName::Custom(name) => f.write_str(name),
|
InterpretName::Custom(name) => f.write_str(name),
|
||||||
InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"),
|
InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"),
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ pub enum RustWebFramework {
|
|||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct RustWebapp {
|
pub struct RustWebapp {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
|
||||||
/// The path to the root of the Rust project to be containerized.
|
/// The path to the root of the Rust project to be containerized.
|
||||||
pub project_root: PathBuf,
|
pub project_root: PathBuf,
|
||||||
pub service_port: u32,
|
pub service_port: u32,
|
||||||
@@ -466,7 +465,6 @@ impl RustWebapp {
|
|||||||
|
|
||||||
let app_name = &self.name;
|
let app_name = &self.name;
|
||||||
let service_port = self.service_port;
|
let service_port = self.service_port;
|
||||||
let chart_version = &self.version;
|
|
||||||
// Create Chart.yaml
|
// Create Chart.yaml
|
||||||
let chart_yaml = format!(
|
let chart_yaml = format!(
|
||||||
r#"
|
r#"
|
||||||
@@ -474,7 +472,7 @@ apiVersion: v2
|
|||||||
name: {chart_name}
|
name: {chart_name}
|
||||||
description: A Helm chart for the {app_name} web application.
|
description: A Helm chart for the {app_name} web application.
|
||||||
type: application
|
type: application
|
||||||
version: {chart_version}
|
version: 0.2.1
|
||||||
appVersion: "{image_tag}"
|
appVersion: "{image_tag}"
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,24 +4,12 @@ use std::net::Ipv4Addr;
|
|||||||
|
|
||||||
use cidr::{Ipv4Cidr, Ipv4Inet};
|
use cidr::{Ipv4Cidr, Ipv4Inet};
|
||||||
pub use discovery::*;
|
pub use discovery::*;
|
||||||
use k8s_openapi::api::{
|
use tokio::time::{Duration, timeout};
|
||||||
apps::v1::{DaemonSet, DaemonSetSpec},
|
|
||||||
core::v1::{
|
|
||||||
Container, EnvVar, Namespace, PodSpec, PodTemplateSpec, ResourceRequirements, SecurityContext,
|
|
||||||
ServiceAccount, Toleration,
|
|
||||||
},
|
|
||||||
rbac::v1::{PolicyRule, Role, RoleBinding, RoleRef, Subject},
|
|
||||||
};
|
|
||||||
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
|
|
||||||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
|
|
||||||
use kube::api::ObjectMeta;
|
|
||||||
use tokio::time::{timeout, Duration};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Version,
|
data::Version,
|
||||||
@@ -29,9 +17,8 @@ use crate::{
|
|||||||
infra::inventory::InventoryRepositoryFactory,
|
infra::inventory::InventoryRepositoryFactory,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::k8s::resource::K8sResourceScore,
|
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{K8sclient, Topology},
|
topology::Topology,
|
||||||
};
|
};
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
|
|
||||||
@@ -303,208 +290,3 @@ impl DiscoverInventoryAgentInterpret {
|
|||||||
info!("CIDR discovery completed");
|
info!("CIDR discovery completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct DeployInventoryAgentScore {
|
|
||||||
pub image: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DeployInventoryAgentScore {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
image: Some("hub.nationtech.io/harmony/harmony_inventory_agent:latest".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology + K8sclient> Score<T> for DeployInventoryAgentScore {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"DeployInventoryAgentScore".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
|
||||||
Box::new(DeployInventoryAgentInterpret {
|
|
||||||
score: self.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct DeployInventoryAgentInterpret {
|
|
||||||
score: DeployInventoryAgentScore,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology + K8sclient> Interpret<T> for DeployInventoryAgentInterpret {
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let namespace_name = "harmony-inventory-agent".to_string();
|
|
||||||
let image = self.score.image.as_ref().unwrap();
|
|
||||||
|
|
||||||
let mut ns_labels = BTreeMap::new();
|
|
||||||
ns_labels.insert("pod-security.kubernetes.io/enforce".to_string(), "privileged".to_string());
|
|
||||||
ns_labels.insert("pod-security.kubernetes.io/audit".to_string(), "privileged".to_string());
|
|
||||||
ns_labels.insert("pod-security.kubernetes.io/warn".to_string(), "privileged".to_string());
|
|
||||||
|
|
||||||
let namespace = Namespace {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some(namespace_name.clone()),
|
|
||||||
labels: Some(ns_labels),
|
|
||||||
..ObjectMeta::default()
|
|
||||||
},
|
|
||||||
..Namespace::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let service_account_name = "harmony-inventory-agent".to_string();
|
|
||||||
let service_account = ServiceAccount {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some(service_account_name.clone()),
|
|
||||||
namespace: Some(namespace_name.clone()),
|
|
||||||
..ObjectMeta::default()
|
|
||||||
},
|
|
||||||
..ServiceAccount::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let role = Role {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("use-privileged-scc".to_string()),
|
|
||||||
namespace: Some(namespace_name.clone()),
|
|
||||||
..ObjectMeta::default()
|
|
||||||
},
|
|
||||||
rules: Some(vec![PolicyRule {
|
|
||||||
api_groups: Some(vec!["security.openshift.io".to_string()]),
|
|
||||||
resources: Some(vec!["securitycontextconstraints".to_string()]),
|
|
||||||
resource_names: Some(vec!["privileged".to_string()]),
|
|
||||||
verbs: vec!["use".to_string()],
|
|
||||||
..PolicyRule::default()
|
|
||||||
}]),
|
|
||||||
..Role::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let role_binding = RoleBinding {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("use-privileged-scc".to_string()),
|
|
||||||
namespace: Some(namespace_name.clone()),
|
|
||||||
..ObjectMeta::default()
|
|
||||||
},
|
|
||||||
subjects: Some(vec![Subject {
|
|
||||||
kind: "ServiceAccount".to_string(),
|
|
||||||
name: service_account_name.clone(),
|
|
||||||
namespace: Some(namespace_name.clone()),
|
|
||||||
..Subject::default()
|
|
||||||
}]),
|
|
||||||
role_ref: RoleRef {
|
|
||||||
api_group: "rbac.authorization.k8s.io".to_string(),
|
|
||||||
kind: "Role".to_string(),
|
|
||||||
name: "use-privileged-scc".to_string(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut daemonset_labels = BTreeMap::new();
|
|
||||||
daemonset_labels.insert("app".to_string(), "harmony-inventory-agent".to_string());
|
|
||||||
|
|
||||||
let daemon_set = DaemonSet {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("harmony-inventory-agent".to_string()),
|
|
||||||
namespace: Some(namespace_name.clone()),
|
|
||||||
labels: Some(daemonset_labels.clone()),
|
|
||||||
..ObjectMeta::default()
|
|
||||||
},
|
|
||||||
spec: Some(DaemonSetSpec {
|
|
||||||
selector: LabelSelector {
|
|
||||||
match_labels: Some(daemonset_labels.clone()),
|
|
||||||
..LabelSelector::default()
|
|
||||||
},
|
|
||||||
template: PodTemplateSpec {
|
|
||||||
metadata: Some(ObjectMeta {
|
|
||||||
labels: Some(daemonset_labels),
|
|
||||||
..ObjectMeta::default()
|
|
||||||
}),
|
|
||||||
spec: Some(PodSpec {
|
|
||||||
service_account_name: Some(service_account_name.clone()),
|
|
||||||
host_network: Some(true),
|
|
||||||
dns_policy: Some("ClusterFirstWithHostNet".to_string()),
|
|
||||||
tolerations: Some(vec![Toleration {
|
|
||||||
key: Some("node-role.kubernetes.io/master".to_string()),
|
|
||||||
operator: Some("Exists".to_string()),
|
|
||||||
effect: Some("NoSchedule".to_string()),
|
|
||||||
..Toleration::default()
|
|
||||||
}]),
|
|
||||||
containers: vec![Container {
|
|
||||||
name: "inventory-agent".to_string(),
|
|
||||||
image: Some(image.to_string()),
|
|
||||||
image_pull_policy: Some("Always".to_string()),
|
|
||||||
env: Some(vec![EnvVar {
|
|
||||||
name: "RUST_LOG".to_string(),
|
|
||||||
value: Some("harmony_inventory_agent=trace,info".to_string()),
|
|
||||||
..EnvVar::default()
|
|
||||||
}]),
|
|
||||||
resources: Some(ResourceRequirements {
|
|
||||||
limits: Some({
|
|
||||||
let mut limits = BTreeMap::new();
|
|
||||||
limits.insert("cpu".to_string(), Quantity("200m".to_string()));
|
|
||||||
limits.insert("memory".to_string(), Quantity("256Mi".to_string()));
|
|
||||||
limits
|
|
||||||
}),
|
|
||||||
requests: Some({
|
|
||||||
let mut requests = BTreeMap::new();
|
|
||||||
requests.insert("cpu".to_string(), Quantity("100m".to_string()));
|
|
||||||
requests.insert("memory".to_string(), Quantity("128Mi".to_string()));
|
|
||||||
requests
|
|
||||||
}),
|
|
||||||
..ResourceRequirements::default()
|
|
||||||
}),
|
|
||||||
security_context: Some(SecurityContext {
|
|
||||||
privileged: Some(true),
|
|
||||||
..SecurityContext::default()
|
|
||||||
}),
|
|
||||||
..Container::default()
|
|
||||||
}],
|
|
||||||
..PodSpec::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
..DaemonSetSpec::default()
|
|
||||||
}),
|
|
||||||
..DaemonSet::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
K8sResourceScore::single(namespace, None)
|
|
||||||
.interpret(_inventory, topology)
|
|
||||||
.await?;
|
|
||||||
K8sResourceScore::single(service_account, Some(namespace_name.clone()))
|
|
||||||
.interpret(_inventory, topology)
|
|
||||||
.await?;
|
|
||||||
K8sResourceScore::single(role, Some(namespace_name.clone()))
|
|
||||||
.interpret(_inventory, topology)
|
|
||||||
.await?;
|
|
||||||
K8sResourceScore::single(role_binding, Some(namespace_name.clone()))
|
|
||||||
.interpret(_inventory, topology)
|
|
||||||
.await?;
|
|
||||||
K8sResourceScore::single(daemon_set, Some(namespace_name.clone()))
|
|
||||||
.interpret(_inventory, topology)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(
|
|
||||||
"Harmony inventory agent successfully deployed".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
|
||||||
InterpretName::DeployInventoryAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "harmony_i18n"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
description = "Minimal compile-time i18n with user-defined languages"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
pub trait Language: Clone + Copy + PartialEq + Eq + Send + Sync + 'static {
|
|
||||||
fn code(&self) -> &'static str;
|
|
||||||
|
|
||||||
fn all() -> &'static [Self]
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Translations<L: Language>:
|
|
||||||
Sized + Clone + Copy + PartialEq + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn for_lang(lang: L) -> Self;
|
|
||||||
|
|
||||||
fn for_code(code: &str) -> Option<Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
for lang in L::all() {
|
|
||||||
if lang.code() == code {
|
|
||||||
return Some(Self::for_lang(*lang));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TranslationsRef<T, L: Language> {
|
|
||||||
translations: T,
|
|
||||||
_lang: PhantomData<L>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, L: Language> TranslationsRef<T, L> {
|
|
||||||
pub fn new(translations: T) -> Self {
|
|
||||||
Self {
|
|
||||||
translations,
|
|
||||||
_lang: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self) -> &T {
|
|
||||||
&self.translations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! translations {
|
|
||||||
(
|
|
||||||
$(#[$struct_attr:meta])*
|
|
||||||
$vis:vis struct $name:ident<$lang_type:ty> {
|
|
||||||
$($field:ident: $ty:ty,)*
|
|
||||||
}
|
|
||||||
$($lang_variant:ident: { $($tfield:ident: $translation:expr,)* },)+
|
|
||||||
) => {
|
|
||||||
$(#[$struct_attr])*
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
$vis struct $name {
|
|
||||||
$(pub $field: $ty,)*
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $crate::Translations<$lang_type> for $name {
|
|
||||||
fn for_lang(lang: $lang_type) -> Self {
|
|
||||||
match lang {
|
|
||||||
$(
|
|
||||||
<$lang_type>::$lang_variant => Self {
|
|
||||||
$($tfield: $translation,)*
|
|
||||||
},
|
|
||||||
)+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! define_language {
|
|
||||||
(
|
|
||||||
$(#[$enum_attr:meta])*
|
|
||||||
$vis:vis enum $name:ident {
|
|
||||||
$(
|
|
||||||
$(#[$variant_attr:meta])*
|
|
||||||
$variant:ident = $code:expr,
|
|
||||||
)+
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
$(#[$enum_attr])*
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
$vis enum $name {
|
|
||||||
$(
|
|
||||||
$(#[$variant_attr])*
|
|
||||||
$variant,
|
|
||||||
)+
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $crate::Language for $name {
|
|
||||||
fn code(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
$(
|
|
||||||
Self::$variant => $code,
|
|
||||||
)+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn all() -> &'static [Self] {
|
|
||||||
&[
|
|
||||||
$(
|
|
||||||
Self::$variant,
|
|
||||||
)+
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $name {
|
|
||||||
pub fn from_code(code: &str) -> Option<Self> {
|
|
||||||
Self::all().iter().find(|l| l.code() == code).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle(&self) -> Self {
|
|
||||||
let all = Self::all();
|
|
||||||
let idx = all.iter().position(|l| l == self).unwrap_or(0);
|
|
||||||
all[(idx + 1) % all.len()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
define_language! {
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum Lang {
|
|
||||||
#[default]
|
|
||||||
En = "en",
|
|
||||||
Fr = "fr",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
translations! {
|
|
||||||
pub struct TestTexts<Lang> {
|
|
||||||
greeting: &'static str,
|
|
||||||
farewell: &'static str,
|
|
||||||
}
|
|
||||||
En: {
|
|
||||||
greeting: "Hello",
|
|
||||||
farewell: "Goodbye",
|
|
||||||
},
|
|
||||||
Fr: {
|
|
||||||
greeting: "Bonjour",
|
|
||||||
farewell: "Au revoir",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_language_enum() {
|
|
||||||
assert_eq!(Lang::En.code(), "en");
|
|
||||||
assert_eq!(Lang::Fr.code(), "fr");
|
|
||||||
assert!(Lang::from_code("en").is_some());
|
|
||||||
assert!(Lang::from_code("de").is_none());
|
|
||||||
assert_eq!(Lang::En.toggle(), Lang::Fr);
|
|
||||||
assert_eq!(Lang::Fr.toggle(), Lang::En);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_translations_for_lang() {
|
|
||||||
let en = TestTexts::for_lang(Lang::En);
|
|
||||||
assert_eq!(en.greeting, "Hello");
|
|
||||||
assert_eq!(en.farewell, "Goodbye");
|
|
||||||
|
|
||||||
let fr = TestTexts::for_lang(Lang::Fr);
|
|
||||||
assert_eq!(fr.greeting, "Bonjour");
|
|
||||||
assert_eq!(fr.farewell, "Au revoir");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_for_code() {
|
|
||||||
let texts = TestTexts::for_code("fr");
|
|
||||||
assert!(texts.is_some());
|
|
||||||
assert_eq!(texts.unwrap().greeting, "Bonjour");
|
|
||||||
|
|
||||||
let none = TestTexts::for_code("de");
|
|
||||||
assert!(none.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -183,7 +183,7 @@ impl OpenbaoSecretStore {
|
|||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
fs::write(path, serde_json::to_string(token)?.as_bytes())?;
|
fs::write(path, token)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user