alien_core/resources/
key.rs1use crate::{
2 error::{ErrorData, Result},
3 resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType},
4};
5use alien_error::AlienError;
6use bon::Builder;
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase", deny_unknown_fields)]
14#[builder(start_fn = new)]
15pub struct Key {
16 #[builder(start_fn)]
18 pub id: String,
19}
20
21impl Key {
22 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("key");
23
24 pub fn id(&self) -> &str {
25 &self.id
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35#[serde(tag = "provider", rename_all = "lowercase", deny_unknown_fields)]
36pub enum KeyFingerprint {
37 Aws { key_arn: String },
39 Gcp { crypto_key_name: String },
41 Azure {
43 vault_resource_id: String,
44 key_name: String,
45 lineage_version_id: String,
46 },
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct KeyOutputs {
54 pub fingerprint: KeyFingerprint,
56 pub wrapping_key_id: String,
58}
59
60impl ResourceDefinition for Key {
61 fn get_resource_type(&self) -> ResourceType {
62 Self::RESOURCE_TYPE
63 }
64
65 fn id(&self) -> &str {
66 &self.id
67 }
68
69 fn get_dependencies(&self) -> Vec<ResourceRef> {
70 Vec::new()
71 }
72
73 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
74 let Some(new_key) = new_config.as_any().downcast_ref::<Self>() else {
75 return Err(AlienError::new(ErrorData::UnexpectedResourceType {
76 resource_id: self.id.clone(),
77 expected: Self::RESOURCE_TYPE,
78 actual: new_config.get_resource_type(),
79 }));
80 };
81
82 if self.id != new_key.id {
83 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
84 resource_id: self.id.clone(),
85 reason: "the 'id' field is immutable".to_string(),
86 }));
87 }
88
89 Ok(())
90 }
91
92 fn as_any(&self) -> &dyn Any {
93 self
94 }
95
96 fn as_any_mut(&mut self) -> &mut dyn Any {
97 self
98 }
99
100 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
101 Box::new(self.clone())
102 }
103
104 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
105 other.as_any().downcast_ref::<Self>() == Some(self)
106 }
107
108 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
109 serde_json::to_value(self)
110 }
111}
112
113impl ResourceOutputsDefinition for KeyOutputs {
114 fn get_resource_type(&self) -> ResourceType {
115 Key::RESOURCE_TYPE
116 }
117
118 fn as_any(&self) -> &dyn Any {
119 self
120 }
121
122 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
123 Box::new(self.clone())
124 }
125
126 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
127 other.as_any().downcast_ref::<Self>() == Some(self)
128 }
129
130 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
131 serde_json::to_value(self)
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn provider_rotation_preserves_the_key_family_fingerprint() {
141 let fingerprint = KeyFingerprint::Gcp {
142 crypto_key_name: "projects/example/locations/us/keyRings/data/cryptoKeys/customer"
143 .to_string(),
144 };
145 let before = KeyOutputs {
146 fingerprint: fingerprint.clone(),
147 wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/1".to_string(),
148 };
149 let after = KeyOutputs {
150 fingerprint,
151 wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/2".to_string(),
152 };
153
154 assert_eq!(before.fingerprint, after.fingerprint);
155 assert_ne!(before.wrapping_key_id, after.wrapping_key_id);
156 }
157}