stackless_integrations/providers/cloudflare/
workers.rs1use std::collections::BTreeMap;
9
10use serde::Serialize;
11use stackless_stripe_projects::catalog::verify::CatalogService;
12use stackless_stripe_projects::provision::ProvisionContext;
13
14use super::CloudflareResource;
15use crate::error::IntegrationError;
16use crate::hostable::{ConfigScope, Hostable, IntegrationHosting};
17
18pub const RESOURCE_KIND: &str = "integration-cloudflare-workers";
19
20#[derive(Debug, Serialize)]
22pub struct WorkersConfig {}
23
24impl CatalogService for WorkersConfig {
25 const REFERENCE: &'static str = "cloudflare/workers";
26}
27
28#[derive(Debug)]
29pub struct CloudflareWorkers;
30
31impl Hostable for CloudflareWorkers {
32 const PROVIDER: &'static str = "cloudflare-workers";
33 const HOSTING: IntegrationHosting = IntegrationHosting::Managed;
34 const CONFIG_SCOPE: ConfigScope = ConfigScope::GlobalOnly;
35 const RESOURCE_KIND: &'static str = RESOURCE_KIND;
36 const OUTPUTS: &'static [&'static str] = super::WORKERS_FAMILY_OUTPUTS;
37}
38
39impl CloudflareResource for CloudflareWorkers {
40 type Config = WorkersConfig;
41 const PROVIDER_PREFIX: &'static str = "CLOUDFLARE";
42 const OUTPUT_FIELDS: &'static [(&'static str, &'static str, bool)] =
44 super::WORKERS_FAMILY_OUTPUT_FIELDS;
45
46 fn build_config(_ctx: &ProvisionContext<'_>) -> Result<WorkersConfig, IntegrationError> {
47 Ok(WorkersConfig {})
48 }
49}
50
51pub fn validate_config(
52 _name: &str,
53 _config: &BTreeMap<String, toml::Value>,
54) -> Result<(), IntegrationError> {
55 Ok(())
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::ProviderOps;
62 use crate::resource::ResourcePayload as CloudflarePayload;
63 use stackless_core::def::StackDef;
64 use stackless_stripe_projects::stripe::{CommandOutput, StripeProjects};
65 use stackless_stripe_projects::test_support::ScriptedRunner;
66
67 fn out(stdout: &str) -> CommandOutput {
68 CommandOutput {
69 status: 0,
70 stdout: stdout.to_owned(),
71 stderr: String::new(),
72 }
73 }
74
75 #[test]
76 fn workers_config_matches_catalog() {
77 const FIXTURE: &str = include_str!(concat!(
78 env!("CARGO_MANIFEST_DIR"),
79 "/../stackless-stripe-projects/tests/fixtures/catalog.json"
80 ));
81 let catalog = stackless_stripe_projects::Catalog::from_json_envelope(FIXTURE).unwrap();
82 let failures = stackless_stripe_projects::verify_service(&catalog, &WorkersConfig {});
83 assert!(
84 failures.is_empty(),
85 "cloudflare/workers catalog gaps:\n{}",
86 failures.join("\n")
87 );
88 }
89
90 const WORKERS_CATALOG_ENVELOPE: &str = r#"{"ok":true,"command":"projects catalog","data":{
91 "last_updated":"2026-06-16T00:00:00Z","services":[{
92 "id":"prvsvc_workers","object":"v2.provisioning.provider_service_detail",
93 "provider_id":"prvdr_cloudflare","provider_name":"Cloudflare","service_id":"workers",
94 "categories":["compute"],"kind":"deployable","scope":"project","availability":"available",
95 "development":false,"livemode":true,"pricing":{"type":"component"},
96 "configuration_schema":{"type":"object","additionalProperties":false,"properties":{}}
97 }]}}"#;
98
99 fn test_def() -> StackDef {
100 StackDef::parse(
101 r#"
102[stack]
103name = "atto"
104[stack.projects.stripe]
105project = "project_1"
106[integrations.edge]
107provider = "cloudflare-workers"
108[services.api]
109source = { repo = "r", ref = "main" }
110env = { CF_SUBDOMAIN = "${integrations.edge.workers_dev_subdomain}" }
111health = { path = "/health" }
112[services.api.local]
113run = "true"
114"#,
115 )
116 .unwrap()
117 }
118
119 #[tokio::test]
120 async fn provision_workers_records_outputs() {
121 let runner = ScriptedRunner::new(vec![
122 out(WORKERS_CATALOG_ENVELOPE),
123 out(r#"{"ok":true,"data":{"project":{"id":"project_1"}}}"#),
124 out(r#"{"ok":true,"data":{"environments":[{"name":"demo"}]}}"#),
125 out(r#"{"ok":true,"data":null}"#),
126 out(r#"{"ok":true,"data":{"services":[]}}"#),
127 out(&serde_json::json!({"ok":true,"data":{"variables":{
128 "CLOUDFLARE_ACCOUNT_ID": "acc_1",
129 "CLOUDFLARE_WORKERS_DEV_SUBDOMAIN": "atto-demo",
130 "CLOUDFLARE_API_BASE_URL": "https://api.cloudflare.com/client/v4",
131 "CLOUDFLARE_DASHBOARD_URL": "https://dash.cloudflare.com/acc_1/workers",
132 "CLOUDFLARE_PLAN_SERVICE_ID": "svc_1"
133 }}})
134 .to_string()),
135 out(r#"{"ok":true,"data":null}"#),
136 out(r#"{"ok":true,"data":null}"#),
137 ]);
138 let dir = tempfile::tempdir().unwrap();
139 std::fs::write(
140 dir.path().join("stackless.toml"),
141 "[stack]\nname=\"atto\"\n",
142 )
143 .unwrap();
144 let stripe = StripeProjects::new(&runner, dir.path());
145
146 let resource = CloudflareWorkers
147 .provision(
148 &stripe.as_dyn(),
149 &test_def(),
150 dir.path(),
151 "demo",
152 "edge",
153 "local",
154 false,
155 )
156 .await
157 .unwrap();
158 assert_eq!(resource.resource_kind, "integration-cloudflare-workers");
159 let payload: CloudflarePayload = serde_json::from_str(&resource.payload).unwrap();
160 assert_eq!(payload.outputs["workers_dev_subdomain"], "atto-demo");
161 assert_eq!(payload.outputs["account_id"], "acc_1");
162 }
163}