fakecloud_sagemaker/lib.rs
1//! Amazon SageMaker (`sagemaker`) awsJson1.1 control-plane service for fakecloud.
2//!
3//! The full ~403-operation Amazon SageMaker Smithy model (SDK id `SageMaker`,
4//! SigV4 signing name `sagemaker`, awsJson1.1 protocol). Every request is a
5//! `POST /` whose operation is selected by the `X-Amz-Target: SageMaker.<Op>`
6//! header, with all inputs carried in the JSON body (no HTTP path / label /
7//! query bindings). The operation table, per-operation input constraints,
8//! output member shapes, list element shapes, resource families and identifier
9//! members are generated from the Smithy model (see `src/generated.rs`,
10//! produced by `scripts/generate-sagemaker-tables.py`), so the control plane
11//! tracks the model exactly.
12//!
13//! **What is real.** Every named resource family — models, endpoints, endpoint
14//! configs, training / processing / transform / labeling / compilation / AutoML
15//! / hyper-parameter-tuning jobs, notebook instances (+ lifecycle configs),
16//! model packages (+ groups), pipelines, feature groups, domains, user
17//! profiles, spaces, apps, images, experiments, trials, actions, artifacts,
18//! contexts, clusters, inference components, monitoring schedules, workteams,
19//! workforces, and the rest — mints a proper ARN, persists its accepted input
20//! attributes plus creation / last-modified timestamps, and echoes them back
21//! on `Describe*` / `List*`. Create is conflict-checked (`ResourceInUse`),
22//! Describe / Update / Delete round-trip by the resource's identifier (name,
23//! id, or ARN), and `AddTags` / `ListTags` / `DeleteTags` persist tags keyed by
24//! ARN. State is account-partitioned and persisted across restarts. Input
25//! validation is model-derived: required members, string `@length`, numeric
26//! `@range`, and `@enum` constraints are enforced, returning SageMaker's
27//! `ValidationException` / `ResourceNotFound` / `ResourceInUse`.
28//!
29//! **Honest emulation choices (documented, not stubbed):**
30//! * This is the SageMaker **control plane** only. There is no ML execution
31//! plane: training / processing / transform / AutoML / tuning jobs are
32//! created, persisted and described, but no container is scheduled, no model
33//! is trained, and no inference endpoint serves traffic. Jobs and endpoints
34//! are not advanced through a live lifecycle by a background scheduler.
35//! * Timestamps are emitted as awsJson1.1 epoch-second JSON numbers.
36//! * A handful of resources whose `Describe*` identifier is a service-minted
37//! Id / ARN distinct from the create-time Name (e.g. `Domain`, `ImageVersion`,
38//! `ModelCardExportJob`) resolve by scanning the family's minted identifiers,
39//! so a describe by the returned Id / ARN still round-trips.
40
41pub mod generated;
42pub mod persistence;
43pub mod service;
44pub mod state;
45pub mod validate;
46
47pub use service::{SageMakerService, SAGEMAKER_ACTIONS};
48pub use state::{
49 SageMakerData, SageMakerSnapshot, SharedSageMakerState, SAGEMAKER_SNAPSHOT_SCHEMA_VERSION,
50};
51
52/// Start a SageMaker pipeline execution from a cross-service delivery (an
53/// EventBridge Scheduler `sagemaker:pipeline` target). Persists a
54/// `PipelineExecution` record keyed by its minted ARN so
55/// DescribePipelineExecution / ListPipelineExecutions resolve it — the same
56/// shape the `StartPipelineExecution` API produces. `pipeline_arn` is
57/// `arn:aws:sagemaker:<region>:<account>:pipeline/<name>`; `parameters` is the
58/// target's `PipelineParameterList` (a JSON array, echoed onto the record).
59pub fn start_pipeline_execution_from_delivery(
60 state: &SharedSageMakerState,
61 pipeline_arn: &str,
62 parameters: &serde_json::Value,
63) {
64 let parts: Vec<&str> = pipeline_arn.split(':').collect();
65 let region = parts.get(3).copied().unwrap_or("us-east-1");
66 let account = parts.get(4).copied().unwrap_or_default();
67 let pipeline_name = pipeline_arn
68 .rsplit_once("pipeline/")
69 .map(|(_, n)| n)
70 .unwrap_or_default();
71 if account.is_empty() || pipeline_name.is_empty() {
72 tracing::warn!(%pipeline_arn, "scheduler->sagemaker: malformed pipeline ARN, skipping");
73 return;
74 }
75
76 let mut g = state.write();
77 let data = g.get_or_create(account);
78 let exec_id = service::mint_id(account, "PipelineExecution", &data.next_seq().to_string());
79 let exec_arn = format!(
80 "arn:aws:sagemaker:{region}:{account}:pipeline/{pipeline_name}/execution/{exec_id}"
81 );
82 let mut record = serde_json::Map::new();
83 record.insert(
84 "PipelineExecutionArn".to_string(),
85 serde_json::Value::String(exec_arn.clone()),
86 );
87 record.insert(
88 "PipelineArn".to_string(),
89 serde_json::Value::String(pipeline_arn.to_string()),
90 );
91 record.insert(
92 "PipelineExecutionStatus".to_string(),
93 serde_json::Value::String("Executing".to_string()),
94 );
95 record.insert("StartTime".to_string(), service::now_epoch());
96 if parameters.is_array() {
97 record.insert("PipelineParameters".to_string(), parameters.clone());
98 }
99 data.put_resource(
100 "PipelineExecution",
101 &exec_arn,
102 serde_json::Value::Object(record),
103 );
104}