Skip to main content

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}