Skip to main content

stormchaser_engine/
artifact.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3use std::time::Duration;
4use stormchaser_model::dsl::Artifact;
5use stormchaser_model::storage::{BackendType, StorageBackend};
6use uuid::Uuid;
7
8/// Generate parking instructions.
9pub async fn generate_parking_instructions(
10    backend: &StorageBackend,
11    run_id: Uuid,
12    storage_name: &str,
13    artifact: &Artifact,
14) -> Result<Value> {
15    match backend.backend_type {
16        BackendType::S3 => {
17            let client = crate::s3::get_s3_client(backend).await?;
18            let bucket = backend.config["bucket"]
19                .as_str()
20                .context("Missing bucket in SFS backend config")?;
21
22            let artifact_key = format!("artifacts/{}/{}/{}", run_id, storage_name, artifact.name);
23            let expires = Duration::from_secs(3600); // 1 hour
24
25            let put_url =
26                crate::s3::generate_presigned_url(&client, bucket, &artifact_key, true, expires)
27                    .await?;
28
29            Ok(serde_json::json!({
30                "backend_type": BackendType::S3,
31                "put_url": put_url,
32                "path": artifact.path,
33                "retention": artifact.retention,
34                "remote_path": artifact_key,
35            }))
36        }
37        BackendType::Oci => {
38            let registry = backend.config["registry"]
39                .as_str()
40                .context("Missing registry in OCI backend config")?;
41            let username = backend.config["username"].as_str();
42            let password = backend.config["password"].as_str();
43
44            let remote_path = format!("{}/{}/{}:{}", registry, run_id, storage_name, artifact.name);
45
46            let mut payload = serde_json::json!({
47                "backend_type": "oci",
48                "registry": registry,
49                "remote_path": remote_path,
50                "path": artifact.path,
51                "retention": artifact.retention,
52            });
53
54            if let (Some(u), Some(p)) = (username, password) {
55                if let Some(map) = payload.as_object_mut() {
56                    map.insert("username".to_string(), Value::String(u.to_string()));
57                    map.insert("password".to_string(), Value::String(p.to_string()));
58                }
59            }
60
61            Ok(payload)
62        }
63        _ => anyhow::bail!(
64            "Unsupported artifact backend type: {:?}",
65            backend.backend_type
66        ),
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use serde_json::json;
74    use stormchaser_model::BackendId;
75
76    #[tokio::test]
77    async fn test_generate_parking_instructions_oci() {
78        let backend = StorageBackend {
79            id: BackendId::new_v4(),
80            name: "oci-registry".into(),
81            description: None,
82            backend_type: BackendType::Oci,
83            config: json!({
84                "registry": "registry.paninfracon.net",
85                "username": "user",
86                "password": "pass"
87            }),
88            aws_assume_role_arn: None,
89            is_default_sfs: false,
90            created_at: chrono::Utc::now(),
91            updated_at: chrono::Utc::now(),
92            ca_cert: None,
93            client_cert: None,
94            client_key: None,
95        };
96
97        let run_id = Uuid::new_v4();
98        let artifact = Artifact {
99            name: "build-bin".into(),
100            path: "bin/".into(),
101            retention: "7d".into(),
102        };
103
104        let instructions = generate_parking_instructions(&backend, run_id, "my-storage", &artifact)
105            .await
106            .unwrap();
107
108        assert_eq!(instructions["backend_type"], "oci");
109        assert_eq!(instructions["registry"], "registry.paninfracon.net");
110        assert_eq!(instructions["username"], "user");
111        assert_eq!(instructions["password"], "pass");
112        assert!(instructions["remote_path"]
113            .as_str()
114            .unwrap()
115            .contains("registry.paninfracon.net"));
116    }
117
118    #[tokio::test]
119    async fn test_generate_parking_instructions_unsupported() {
120        let backend = StorageBackend {
121            id: BackendId::new_v4(),
122            name: "fs-backend".into(),
123            description: None,
124            backend_type: BackendType::Jfrog,
125            config: json!({}),
126            aws_assume_role_arn: None,
127            is_default_sfs: false,
128            created_at: chrono::Utc::now(),
129            updated_at: chrono::Utc::now(),
130            ca_cert: None,
131            client_cert: None,
132            client_key: None,
133        };
134
135        let result = generate_parking_instructions(
136            &backend,
137            Uuid::new_v4(),
138            "st",
139            &Artifact {
140                name: "n".into(),
141                path: "p".into(),
142                retention: "1d".into(),
143            },
144        )
145        .await;
146
147        assert!(result.is_err());
148    }
149}