1use std::sync::Arc;
13
14use async_trait::async_trait;
15use http::StatusCode;
16use serde_json::{Map, Value};
17use tokio::sync::Mutex as AsyncMutex;
18
19use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
20use fakecloud_persistence::SnapshotStore;
21
22use crate::generated::{OpMeta, Verb, OPS};
23use crate::persistence::save_snapshot;
24use crate::state::SharedSageMakerState;
25
26mod engine;
27mod special;
28#[cfg(test)]
29mod tests;
30
31pub use crate::generated::ACTIONS as SAGEMAKER_ACTIONS;
33
34pub const COMMON_ERRORS: &[&str] = &[
40 "ResourceNotFound",
41 "ResourceInUse",
42 "ResourceLimitExceeded",
43 "ConflictException",
44 "ValidationException",
45];
46
47pub struct SageMakerService {
48 state: SharedSageMakerState,
49 snapshot_store: Option<Arc<dyn SnapshotStore>>,
50 snapshot_lock: Arc<AsyncMutex<()>>,
51}
52
53impl SageMakerService {
54 pub fn new(state: SharedSageMakerState) -> Self {
55 Self {
56 state,
57 snapshot_store: None,
58 snapshot_lock: Arc::new(AsyncMutex::new(())),
59 }
60 }
61
62 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
63 self.snapshot_store = Some(store);
64 self
65 }
66
67 async fn save(&self) {
68 save_snapshot(
69 &self.state,
70 self.snapshot_store.clone(),
71 &self.snapshot_lock,
72 )
73 .await;
74 }
75
76 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
78 let store = self.snapshot_store.clone()?;
79 let state = self.state.clone();
80 let lock = self.snapshot_lock.clone();
81 Some(Arc::new(move || {
82 let state = state.clone();
83 let store = store.clone();
84 let lock = lock.clone();
85 Box::pin(async move {
86 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
87 })
88 }))
89 }
90}
91
92#[async_trait]
93impl AwsService for SageMakerService {
94 fn service_name(&self) -> &str {
95 "sagemaker"
96 }
97
98 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
99 let Some(meta) = OPS.iter().find(|m| m.op == req.action) else {
100 return Err(AwsServiceError::action_not_implemented(
101 "sagemaker",
102 &req.action,
103 ));
104 };
105 let (resp, mutated) = self.dispatch(meta, &req)?;
106 if mutated && resp.status.is_success() {
107 self.save().await;
108 }
109 Ok(resp)
110 }
111
112 fn supported_actions(&self) -> &[&str] {
113 SAGEMAKER_ACTIONS
114 }
115}
116
117pub(crate) struct Ctx {
119 pub account: String,
120 pub region: String,
121}
122
123impl SageMakerService {
124 fn dispatch(
125 &self,
126 meta: &'static OpMeta,
127 req: &AwsRequest,
128 ) -> Result<(AwsResponse, bool), AwsServiceError> {
129 let ctx = Ctx {
130 account: req.account_id.clone(),
131 region: if req.region.is_empty() {
132 "us-east-1".to_string()
133 } else {
134 req.region.clone()
135 },
136 };
137 let body = parse_body(&req.body);
138 crate::validate::validate(meta, &body)?;
139
140 if let Some(res) = special::dispatch(self, meta, &ctx, &body)? {
142 return Ok(res);
143 }
144
145 match meta.verb {
146 Verb::Create => {
147 let mut g = self.state.write();
148 let data = g.get_or_create(&ctx.account);
149 Ok((engine::create(data, &ctx, meta, &body)?, true))
150 }
151 Verb::Update => {
152 let mut g = self.state.write();
153 let data = g.get_or_create(&ctx.account);
154 Ok((engine::update(data, &ctx, meta, &body)?, true))
155 }
156 Verb::Delete => {
157 let mut g = self.state.write();
158 let data = g.get_or_create(&ctx.account);
159 Ok((engine::delete(data, &ctx, meta, &body), true))
160 }
161 Verb::Get => {
162 let g = self.state.read();
163 let data = g.get(&ctx.account);
164 Ok((engine::get(data, &ctx, meta, &body)?, false))
165 }
166 Verb::List => {
167 let g = self.state.read();
168 let data = g.get(&ctx.account);
169 Ok((engine::list(data, &ctx, meta, &body), false))
170 }
171 Verb::Action => {
172 Ok((engine::action(&ctx, meta, &body), false))
176 }
177 }
178 }
179}
180
181pub(crate) fn ok_json(v: Value) -> AwsResponse {
184 AwsResponse::json_value(StatusCode::OK, v)
185}
186
187pub(crate) fn parse_body(body: &[u8]) -> Map<String, Value> {
188 if body.is_empty() {
189 return Map::new();
190 }
191 match serde_json::from_slice::<Value>(body) {
192 Ok(Value::Object(m)) => m,
193 _ => Map::new(),
194 }
195}
196
197pub(crate) fn now_epoch() -> Value {
203 let millis = chrono::Utc::now().timestamp_millis();
204 Value::from(millis as f64 / 1000.0)
205}
206
207pub(crate) fn mint_arn(ctx: &Ctx, arn_path: &str, name: &str) -> String {
208 format!(
209 "arn:aws:sagemaker:{}:{}:{}/{}",
210 ctx.region, ctx.account, arn_path, name
211 )
212}
213
214pub(crate) fn mint_id(account: &str, family: &str, seed: &str) -> String {
216 let base = format!("{account}:{family}:{seed}");
217 let h = fnv(&base);
218 let h2 = fnv(&format!("{base}:2"));
219 format!(
220 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
221 (h >> 32) as u32,
222 (h >> 16) as u16,
223 h as u16,
224 (h2 >> 48) as u16,
225 h2 & 0xffff_ffff_ffff
226 )
227}
228
229fn fnv(s: &str) -> u64 {
230 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
231 for b in s.bytes() {
232 h ^= b as u64;
233 h = h.wrapping_mul(0x0100_0000_01b3);
234 }
235 h
236}
237
238pub(crate) fn not_found(msg: impl Into<String>) -> AwsServiceError {
239 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "ResourceNotFound", msg)
240}
241
242pub(crate) fn in_use(msg: impl Into<String>) -> AwsServiceError {
243 AwsServiceError::aws_error(StatusCode::CONFLICT, "ResourceInUse", msg)
244}