cloud_tasks_emulator/
lib.rs1mod api;
2mod filter;
3mod names;
4mod oidc;
5mod rest;
6mod scheduler;
7mod state;
8
9use std::sync::{Arc, LazyLock, Mutex};
10use std::time::Duration;
11
12use axum::extract::{Path, State};
13use axum::http::StatusCode;
14use axum::response::{IntoResponse, Response};
15use axum::routing::{get, post};
16use prost_reflect::DescriptorPool;
17use tokio::net::TcpListener;
18use tokio::sync::Notify;
19use tonic::service::Routes;
20use tonic::transport::server::TcpIncoming;
21
22use crate::proto::google::cloud::tasks::v2::cloud_tasks_server::{CloudTasks, CloudTasksServer};
23use crate::proto::google::cloud::tasks::v2::{CreateQueueRequest, Queue};
24use crate::proto::google::longrunning::operations_server::OperationsServer;
25use crate::state::{Emulator, QueueState};
26
27pub mod proto {
28 pub mod google {
29 pub mod api {
30 tonic::include_proto!("google.api");
31 }
32 pub mod cloud {
33 pub mod tasks {
34 pub mod v2 {
35 tonic::include_proto!("google.cloud.tasks.v2");
36 }
37 }
38 }
39 pub mod iam {
40 pub mod v1 {
41 tonic::include_proto!("google.iam.v1");
42 }
43 }
44 pub mod longrunning {
45 tonic::include_proto!("google.longrunning");
46 }
47 pub mod rpc {
48 tonic::include_proto!("google.rpc");
49 }
50 pub mod r#type {
51 tonic::include_proto!("google.r#type");
52 }
53 }
54}
55
56static DESCRIPTORS: LazyLock<DescriptorPool> = LazyLock::new(|| {
57 DescriptorPool::decode(include_bytes!(concat!(env!("OUT_DIR"), "/descriptors.bin")).as_slice())
58 .expect("build.rs encodes the descriptors")
59});
60
61pub async fn serve(
63 listener: TcpListener,
64 issuer: String,
65 queues: Vec<String>,
66) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
67 let emulator = Arc::new(Emulator {
68 state: Mutex::new(startup_state(&queues).await?),
69 wake: Notify::new(),
70 });
71 let issuer = Arc::new(oidc::Issuer::new(issuer));
72 tokio::spawn(scheduler::run(emulator.clone(), issuer.clone()));
73 let discovery = axum::Router::new()
74 .route("/.well-known/openid-configuration", get(oidc::discovery))
75 .route("/jwks", get(oidc::jwks))
76 .with_state(issuer);
77 let idle = axum::Router::new()
78 .route("/emulator/{*path}", get(wait_idle))
79 .with_state(emulator.clone());
80 let reset = axum::Router::new()
81 .route("/emulator:reset", post(reset))
82 .with_state((emulator.clone(), Arc::new(queues)));
83 let grpc = Routes::new(CloudTasksServer::from_arc(emulator.clone()))
84 .add_service(OperationsServer::from_arc(emulator));
85 let routes = grpc
86 .clone()
87 .into_axum_router()
88 .merge(discovery)
89 .merge(idle)
90 .merge(reset)
91 .merge(rest::router(grpc));
92 tonic::transport::Server::builder()
93 .accept_http1(true)
94 .add_routes(routes.into())
95 .serve_with_incoming(TcpIncoming::from(listener))
96 .await?;
97 Ok(())
98}
99
100async fn startup_state(queues: &[String]) -> Result<state::State, String> {
102 let emulator = Emulator::default();
103 for name in queues {
104 let request = CreateQueueRequest {
105 parent: name
106 .rsplit_once("/queues/")
107 .map_or(name.as_str(), |(parent, _)| parent)
108 .to_owned(),
109 queue: Some(Queue {
110 name: name.clone(),
111 ..Default::default()
112 }),
113 };
114 emulator
115 .create_queue(tonic::Request::new(request))
116 .await
117 .map_err(|status| format!("Cannot create {name}: {}", status.message()))?;
118 }
119 Ok(emulator.state.into_inner().unwrap())
120}
121
122async fn reset(State((emulator, queues)): State<(Arc<Emulator>, Arc<Vec<String>>)>) -> StatusCode {
124 let fresh = startup_state(&queues)
125 .await
126 .expect("startup created these queues already");
127 emulator.state.lock().unwrap().reset(fresh);
128 StatusCode::NO_CONTENT
129}
130
131async fn wait_idle(State(emulator): State<Arc<Emulator>>, Path(path): Path<String>) -> Response {
133 let Some(name) = path.strip_suffix(":waitIdle") else {
134 return StatusCode::NOT_FOUND.into_response();
135 };
136 loop {
137 {
138 let state = emulator.state.lock().unwrap();
139 let Some(queue) = state.queues.get(name) else {
140 return (
141 StatusCode::NOT_FOUND,
142 format!("Queue {name} does not exist"),
143 )
144 .into_response();
145 };
146 if queue.tasks.is_empty() {
147 return StatusCode::NO_CONTENT.into_response();
148 }
149 if queue.state == QueueState::Paused {
150 return (
151 StatusCode::CONFLICT,
152 format!("Queue {name} is paused with tasks left"),
153 )
154 .into_response();
155 }
156 }
157 tokio::time::sleep(Duration::from_millis(10)).await;
158 }
159}