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 replace_after_failed_create_is_safe(&self) -> bool {
70 false
72 }
73
74 fn get_dependencies(&self) -> Vec<ResourceRef> {
75 Vec::new()
76 }
77
78 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
79 let Some(new_key) = new_config.as_any().downcast_ref::<Self>() else {
80 return Err(AlienError::new(ErrorData::UnexpectedResourceType {
81 resource_id: self.id.clone(),
82 expected: Self::RESOURCE_TYPE,
83 actual: new_config.get_resource_type(),
84 }));
85 };
86
87 if self.id != new_key.id {
88 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
89 resource_id: self.id.clone(),
90 reason: "the 'id' field is immutable".to_string(),
91 }));
92 }
93
94 Ok(())
95 }
96
97 fn as_any(&self) -> &dyn Any {
98 self
99 }
100
101 fn as_any_mut(&mut self) -> &mut dyn Any {
102 self
103 }
104
105 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
106 Box::new(self.clone())
107 }
108
109 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
110 other.as_any().downcast_ref::<Self>() == Some(self)
111 }
112
113 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
114 serde_json::to_value(self)
115 }
116}
117
118impl ResourceOutputsDefinition for KeyOutputs {
119 fn get_resource_type(&self) -> ResourceType {
120 Key::RESOURCE_TYPE
121 }
122
123 fn as_any(&self) -> &dyn Any {
124 self
125 }
126
127 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
128 Box::new(self.clone())
129 }
130
131 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
132 other.as_any().downcast_ref::<Self>() == Some(self)
133 }
134
135 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
136 serde_json::to_value(self)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn provider_rotation_preserves_the_key_family_fingerprint() {
146 let fingerprint = KeyFingerprint::Gcp {
147 crypto_key_name: "projects/example/locations/us/keyRings/data/cryptoKeys/customer"
148 .to_string(),
149 };
150 let before = KeyOutputs {
151 fingerprint: fingerprint.clone(),
152 wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/1".to_string(),
153 };
154 let after = KeyOutputs {
155 fingerprint,
156 wrapping_key_id: "projects/example/locations/us/keyRings/data/cryptoKeys/customer/cryptoKeyVersions/2".to_string(),
157 };
158
159 assert_eq!(before.fingerprint, after.fingerprint);
160 assert_ne!(before.wrapping_key_id, after.wrapping_key_id);
161 }
162}