1use async_trait::async_trait;
14use chrono::{DateTime, Utc};
15use http::header::{HeaderName, HeaderValue};
16use http::{Method, StatusCode};
17use serde_json::{json, Map, Value};
18use std::sync::Arc;
19use tokio::sync::Mutex as AsyncMutex;
20
21use fakecloud_core::pagination::paginate_checked;
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::state::{
26 application_arn, deployment_strategy_arn, environment_arn, extension_arn,
27 extension_association_arn, profile_arn, ApplicationRecord, DeploymentRecord, EnvironmentRecord,
28 ExperimentRecord, HostedVersionRecord, ProfileRecord, SessionRecord, SharedAppConfigState,
29 TagMap,
30};
31
32pub const APPCONFIG_ACTIONS: &[&str] = &[
33 "CreateApplication",
35 "CreateConfigurationProfile",
36 "CreateDeploymentStrategy",
37 "CreateEnvironment",
38 "CreateExperimentDefinition",
39 "CreateExtension",
40 "CreateExtensionAssociation",
41 "CreateHostedConfigurationVersion",
42 "DeleteApplication",
43 "DeleteConfigurationProfile",
44 "DeleteDeploymentStrategy",
45 "DeleteEnvironment",
46 "DeleteExperimentDefinition",
47 "DeleteExtension",
48 "DeleteExtensionAssociation",
49 "DeleteHostedConfigurationVersion",
50 "GetAccountSettings",
51 "GetApplication",
52 "GetConfiguration",
53 "GetConfigurationProfile",
54 "GetDeployment",
55 "GetDeploymentStrategy",
56 "GetEnvironment",
57 "GetExperimentDefinition",
58 "GetExperimentRun",
59 "GetExtension",
60 "GetExtensionAssociation",
61 "GetHostedConfigurationVersion",
62 "ListApplications",
63 "ListConfigurationProfiles",
64 "ListDeploymentStrategies",
65 "ListDeployments",
66 "ListEnvironments",
67 "ListExperimentDefinitions",
68 "ListExperimentRunEvents",
69 "ListExperimentRuns",
70 "ListExtensionAssociations",
71 "ListExtensions",
72 "ListHostedConfigurationVersions",
73 "ListTagsForResource",
74 "StartDeployment",
75 "StartExperimentRun",
76 "StopDeployment",
77 "StopExperimentRun",
78 "TagResource",
79 "UntagResource",
80 "UpdateAccountSettings",
81 "UpdateApplication",
82 "UpdateConfigurationProfile",
83 "UpdateDeploymentStrategy",
84 "UpdateEnvironment",
85 "UpdateExperimentDefinition",
86 "UpdateExperimentRun",
87 "UpdateExtension",
88 "UpdateExtensionAssociation",
89 "ValidateConfiguration",
90 "GetLatestConfiguration",
92 "StartConfigurationSession",
93];
94
95pub struct AppConfigService {
96 state: SharedAppConfigState,
97 snapshot_store: Option<Arc<dyn SnapshotStore>>,
98 snapshot_lock: Arc<AsyncMutex<()>>,
99}
100
101type Labels = Vec<String>;
103
104impl AppConfigService {
105 pub fn new(state: SharedAppConfigState) -> Self {
106 Self {
107 state,
108 snapshot_store: None,
109 snapshot_lock: Arc::new(AsyncMutex::new(())),
110 }
111 }
112
113 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
114 self.snapshot_store = Some(store);
115 self
116 }
117
118 async fn save_snapshot(&self) {
119 crate::persistence::save_snapshot(
120 &self.state,
121 self.snapshot_store.clone(),
122 &self.snapshot_lock,
123 )
124 .await;
125 }
126
127 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
129 let store = self.snapshot_store.clone()?;
130 let state = self.state.clone();
131 let lock = self.snapshot_lock.clone();
132 Some(Arc::new(move || {
133 let state = state.clone();
134 let store = store.clone();
135 let lock = lock.clone();
136 Box::pin(async move {
137 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
138 })
139 }))
140 }
141
142 fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Labels)> {
144 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
145 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
146 let segs: Vec<&str> = if trimmed.is_empty() {
147 Vec::new()
148 } else {
149 trimmed.split('/').collect()
150 };
151 let m = &req.method;
152 let d = |s: &str| decode(s);
153 let one = |a| Some((a, Vec::new()));
154 macro_rules! l {
155 ($a:expr, $($x:expr),*) => { Some(($a, vec![$($x),*])) };
156 }
157 let patch = Method::PATCH;
158 match (m, segs.as_slice()) {
159 (&Method::POST, ["applications"]) => one("CreateApplication"),
161 (&Method::GET, ["applications"]) => one("ListApplications"),
162 (&Method::GET, ["applications", app]) => l!("GetApplication", d(app)),
163 (mth, ["applications", app]) if *mth == patch => l!("UpdateApplication", d(app)),
164 (&Method::DELETE, ["applications", app]) => l!("DeleteApplication", d(app)),
165 (&Method::POST, ["applications", app, "configurationprofiles"]) => {
167 l!("CreateConfigurationProfile", d(app))
168 }
169 (&Method::GET, ["applications", app, "configurationprofiles"]) => {
170 l!("ListConfigurationProfiles", d(app))
171 }
172 (&Method::GET, ["applications", app, "configurationprofiles", p]) => {
173 l!("GetConfigurationProfile", d(app), d(p))
174 }
175 (mth, ["applications", app, "configurationprofiles", p]) if *mth == patch => {
176 l!("UpdateConfigurationProfile", d(app), d(p))
177 }
178 (&Method::DELETE, ["applications", app, "configurationprofiles", p]) => {
179 l!("DeleteConfigurationProfile", d(app), d(p))
180 }
181 (&Method::POST, ["applications", app, "configurationprofiles", p, "validators"]) => {
182 l!("ValidateConfiguration", d(app), d(p))
183 }
184 (
186 &Method::POST,
187 ["applications", app, "configurationprofiles", p, "hostedconfigurationversions"],
188 ) => l!("CreateHostedConfigurationVersion", d(app), d(p)),
189 (
190 &Method::GET,
191 ["applications", app, "configurationprofiles", p, "hostedconfigurationversions"],
192 ) => l!("ListHostedConfigurationVersions", d(app), d(p)),
193 (
194 &Method::GET,
195 ["applications", app, "configurationprofiles", p, "hostedconfigurationversions", v],
196 ) => l!("GetHostedConfigurationVersion", d(app), d(p), d(v)),
197 (
198 &Method::DELETE,
199 ["applications", app, "configurationprofiles", p, "hostedconfigurationversions", v],
200 ) => l!("DeleteHostedConfigurationVersion", d(app), d(p), d(v)),
201 (&Method::POST, ["applications", app, "environments"]) => {
203 l!("CreateEnvironment", d(app))
204 }
205 (&Method::GET, ["applications", app, "environments"]) => l!("ListEnvironments", d(app)),
206 (&Method::GET, ["applications", app, "environments", e]) => {
207 l!("GetEnvironment", d(app), d(e))
208 }
209 (mth, ["applications", app, "environments", e]) if *mth == patch => {
210 l!("UpdateEnvironment", d(app), d(e))
211 }
212 (&Method::DELETE, ["applications", app, "environments", e]) => {
213 l!("DeleteEnvironment", d(app), d(e))
214 }
215 (&Method::POST, ["applications", app, "environments", e, "deployments"]) => {
217 l!("StartDeployment", d(app), d(e))
218 }
219 (&Method::GET, ["applications", app, "environments", e, "deployments"]) => {
220 l!("ListDeployments", d(app), d(e))
221 }
222 (&Method::GET, ["applications", app, "environments", e, "deployments", n]) => {
223 l!("GetDeployment", d(app), d(e), d(n))
224 }
225 (&Method::DELETE, ["applications", app, "environments", e, "deployments", n]) => {
226 l!("StopDeployment", d(app), d(e), d(n))
227 }
228 (&Method::GET, ["applications", app, "environments", e, "configurations", c]) => {
230 l!("GetConfiguration", d(app), d(e), d(c))
231 }
232 (&Method::POST, ["deploymentstrategies"]) => one("CreateDeploymentStrategy"),
234 (&Method::GET, ["deploymentstrategies"]) => one("ListDeploymentStrategies"),
235 (&Method::GET, ["deploymentstrategies", id]) => l!("GetDeploymentStrategy", d(id)),
236 (mth, ["deploymentstrategies", id]) if *mth == patch => {
237 l!("UpdateDeploymentStrategy", d(id))
238 }
239 (&Method::DELETE, ["deployementstrategies", id]) => {
241 l!("DeleteDeploymentStrategy", d(id))
242 }
243 (&Method::POST, ["extensions"]) => one("CreateExtension"),
245 (&Method::GET, ["extensions"]) => one("ListExtensions"),
246 (&Method::GET, ["extensions", id]) => l!("GetExtension", d(id)),
247 (mth, ["extensions", id]) if *mth == patch => l!("UpdateExtension", d(id)),
248 (&Method::DELETE, ["extensions", id]) => l!("DeleteExtension", d(id)),
249 (&Method::POST, ["extensionassociations"]) => one("CreateExtensionAssociation"),
251 (&Method::GET, ["extensionassociations"]) => one("ListExtensionAssociations"),
252 (&Method::GET, ["extensionassociations", id]) => {
253 l!("GetExtensionAssociation", d(id))
254 }
255 (mth, ["extensionassociations", id]) if *mth == patch => {
256 l!("UpdateExtensionAssociation", d(id))
257 }
258 (&Method::DELETE, ["extensionassociations", id]) => {
259 l!("DeleteExtensionAssociation", d(id))
260 }
261 (&Method::GET, ["experimentdefinitions"]) => one("ListExperimentDefinitions"),
263 (&Method::POST, ["applications", app, "experimentdefinitions"]) => {
264 l!("CreateExperimentDefinition", d(app))
265 }
266 (&Method::GET, ["applications", app, "experimentdefinitions", id]) => {
267 l!("GetExperimentDefinition", d(app), d(id))
268 }
269 (mth, ["applications", app, "experimentdefinitions", id]) if *mth == patch => {
270 l!("UpdateExperimentDefinition", d(app), d(id))
271 }
272 (&Method::DELETE, ["applications", app, "experimentdefinitions", id]) => {
273 l!("DeleteExperimentDefinition", d(app), d(id))
274 }
275 (
277 &Method::POST,
278 ["applications", app, "experimentdefinitions", id, "experimentruns"],
279 ) => {
280 l!("StartExperimentRun", d(app), d(id))
281 }
282 (
283 &Method::GET,
284 ["applications", app, "experimentdefinitions", id, "experimentruns"],
285 ) => {
286 l!("ListExperimentRuns", d(app), d(id))
287 }
288 (
289 &Method::GET,
290 ["applications", app, "experimentdefinitions", id, "experimentruns", run],
291 ) => l!("GetExperimentRun", d(app), d(id), d(run)),
292 (
293 mth,
294 ["applications", app, "experimentdefinitions", id, "experimentruns", run, "update"],
295 ) if *mth == patch => l!("UpdateExperimentRun", d(app), d(id), d(run)),
296 (
297 mth,
298 ["applications", app, "experimentdefinitions", id, "experimentruns", run, "stop"],
299 ) if *mth == patch => l!("StopExperimentRun", d(app), d(id), d(run)),
300 (
301 &Method::GET,
302 ["applications", app, "experimentdefinitions", id, "experimentruns", run, "events"],
303 ) => l!("ListExperimentRunEvents", d(app), d(id), d(run)),
304 (&Method::GET, ["settings"]) => one("GetAccountSettings"),
306 (mth, ["settings"]) if *mth == patch => one("UpdateAccountSettings"),
307 (&Method::GET, ["tags", arn]) => l!("ListTagsForResource", d(arn)),
309 (&Method::POST, ["tags", arn]) => l!("TagResource", d(arn)),
310 (&Method::DELETE, ["tags", arn]) => l!("UntagResource", d(arn)),
311 (&Method::POST, ["configurationsessions"]) => one("StartConfigurationSession"),
313 (&Method::GET, ["configuration"]) => one("GetLatestConfiguration"),
314 _ => None,
315 }
316 }
317}
318
319const MUTATING: &[&str] = &[
320 "CreateApplication",
321 "CreateConfigurationProfile",
322 "CreateDeploymentStrategy",
323 "CreateEnvironment",
324 "CreateExperimentDefinition",
325 "CreateExtension",
326 "CreateExtensionAssociation",
327 "CreateHostedConfigurationVersion",
328 "DeleteApplication",
329 "DeleteConfigurationProfile",
330 "DeleteDeploymentStrategy",
331 "DeleteEnvironment",
332 "DeleteExperimentDefinition",
333 "DeleteExtension",
334 "DeleteExtensionAssociation",
335 "DeleteHostedConfigurationVersion",
336 "StartDeployment",
337 "StartExperimentRun",
338 "StopDeployment",
339 "StopExperimentRun",
340 "TagResource",
341 "UntagResource",
342 "UpdateAccountSettings",
343 "UpdateApplication",
344 "UpdateConfigurationProfile",
345 "UpdateDeploymentStrategy",
346 "UpdateEnvironment",
347 "UpdateExperimentDefinition",
348 "UpdateExperimentRun",
349 "UpdateExtension",
350 "UpdateExtensionAssociation",
351 "StartConfigurationSession",
352];
353
354#[async_trait]
355impl AwsService for AppConfigService {
356 fn service_name(&self) -> &str {
357 "appconfig"
358 }
359
360 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
361 let (action, labels) = Self::resolve_action(&req).ok_or_else(|| {
362 AwsServiceError::aws_error(
363 StatusCode::NOT_FOUND,
364 "UnknownOperationException",
365 format!("Unknown operation: {} {}", req.method, req.raw_path),
366 )
367 })?;
368
369 let result = self.dispatch(action, &labels, &req);
370
371 if MUTATING.contains(&action)
372 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
373 {
374 self.save_snapshot().await;
375 }
376 result
377 }
378
379 fn supported_actions(&self) -> &[&str] {
380 APPCONFIG_ACTIONS
381 }
382}
383
384impl AppConfigService {
385 #[allow(clippy::too_many_lines)]
386 fn dispatch(
387 &self,
388 action: &str,
389 l: &[String],
390 req: &AwsRequest,
391 ) -> Result<AwsResponse, AwsServiceError> {
392 crate::validation::validate(action, l, req)?;
393 match action {
394 "CreateApplication" => self.create_application(req),
396 "GetApplication" => self.get_application(req, &l[0]),
397 "UpdateApplication" => self.update_application(req, &l[0]),
398 "DeleteApplication" => self.delete_application(req, &l[0]),
399 "ListApplications" => self.list_applications(req),
400 "CreateConfigurationProfile" => self.create_profile(req, &l[0]),
402 "GetConfigurationProfile" => self.get_profile(req, &l[0], &l[1]),
403 "UpdateConfigurationProfile" => self.update_profile(req, &l[0], &l[1]),
404 "DeleteConfigurationProfile" => self.delete_profile(req, &l[0], &l[1]),
405 "ListConfigurationProfiles" => self.list_profiles(req, &l[0]),
406 "ValidateConfiguration" => self.validate_configuration(req, &l[0], &l[1]),
407 "CreateHostedConfigurationVersion" => self.create_hosted_version(req, &l[0], &l[1]),
409 "GetHostedConfigurationVersion" => self.get_hosted_version(req, &l[0], &l[1], &l[2]),
410 "DeleteHostedConfigurationVersion" => {
411 self.delete_hosted_version(req, &l[0], &l[1], &l[2])
412 }
413 "ListHostedConfigurationVersions" => self.list_hosted_versions(req, &l[0], &l[1]),
414 "CreateEnvironment" => self.create_environment(req, &l[0]),
416 "GetEnvironment" => self.get_environment(req, &l[0], &l[1]),
417 "UpdateEnvironment" => self.update_environment(req, &l[0], &l[1]),
418 "DeleteEnvironment" => self.delete_environment(req, &l[0], &l[1]),
419 "ListEnvironments" => self.list_environments(req, &l[0]),
420 "StartDeployment" => self.start_deployment(req, &l[0], &l[1]),
422 "GetDeployment" => self.get_deployment(req, &l[0], &l[1], &l[2]),
423 "StopDeployment" => self.stop_deployment(req, &l[0], &l[1], &l[2]),
424 "ListDeployments" => self.list_deployments(req, &l[0], &l[1]),
425 "GetConfiguration" => self.get_configuration(req, &l[0], &l[1], &l[2]),
426 "CreateDeploymentStrategy" => self.create_deployment_strategy(req),
428 "GetDeploymentStrategy" => self.get_deployment_strategy(req, &l[0]),
429 "UpdateDeploymentStrategy" => self.update_deployment_strategy(req, &l[0]),
430 "DeleteDeploymentStrategy" => self.delete_deployment_strategy(req, &l[0]),
431 "ListDeploymentStrategies" => self.list_deployment_strategies(req),
432 "CreateExtension" => self.create_extension(req),
434 "GetExtension" => self.get_extension(req, &l[0]),
435 "UpdateExtension" => self.update_extension(req, &l[0]),
436 "DeleteExtension" => self.delete_extension(req, &l[0]),
437 "ListExtensions" => self.list_extensions(req),
438 "CreateExtensionAssociation" => self.create_extension_association(req),
440 "GetExtensionAssociation" => self.get_extension_association(req, &l[0]),
441 "UpdateExtensionAssociation" => self.update_extension_association(req, &l[0]),
442 "DeleteExtensionAssociation" => self.delete_extension_association(req, &l[0]),
443 "ListExtensionAssociations" => self.list_extension_associations(req),
444 "CreateExperimentDefinition" => self.create_experiment_definition(req, &l[0]),
446 "GetExperimentDefinition" => self.get_experiment_definition(req, &l[0], &l[1]),
447 "UpdateExperimentDefinition" => self.update_experiment_definition(req, &l[0], &l[1]),
448 "DeleteExperimentDefinition" => self.delete_experiment_definition(req, &l[0], &l[1]),
449 "ListExperimentDefinitions" => self.list_experiment_definitions(req),
450 "StartExperimentRun" => self.start_experiment_run(req, &l[0], &l[1]),
451 "GetExperimentRun" => self.get_experiment_run(req, &l[0], &l[1], &l[2]),
452 "UpdateExperimentRun" => self.update_experiment_run(req, &l[0], &l[1], &l[2]),
453 "StopExperimentRun" => self.stop_experiment_run(req, &l[0], &l[1], &l[2]),
454 "ListExperimentRuns" => self.list_experiment_runs(req, &l[0], &l[1]),
455 "ListExperimentRunEvents" => self.list_experiment_run_events(req, &l[0], &l[1], &l[2]),
456 "GetAccountSettings" => self.get_account_settings(req),
458 "UpdateAccountSettings" => self.update_account_settings(req),
459 "ListTagsForResource" => self.list_tags(req, &l[0]),
461 "TagResource" => self.tag_resource(req, &l[0]),
462 "UntagResource" => self.untag_resource(req, &l[0]),
463 "StartConfigurationSession" => self.start_configuration_session(req),
465 "GetLatestConfiguration" => self.get_latest_configuration(req),
466 _ => Err(AwsServiceError::action_not_implemented("appconfig", action)),
467 }
468 }
469}
470
471include!("handlers.rs");