1use async_trait::async_trait;
15use chrono::{DateTime, Utc};
16use http::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::{CodeDeployState, SharedCodeDeployState};
26use crate::validate;
27
28pub const CODEDEPLOY_ACTIONS: &[&str] = &[
29 "AddTagsToOnPremisesInstances",
30 "BatchGetApplicationRevisions",
31 "BatchGetApplications",
32 "BatchGetDeploymentGroups",
33 "BatchGetDeploymentInstances",
34 "BatchGetDeploymentTargets",
35 "BatchGetDeployments",
36 "BatchGetOnPremisesInstances",
37 "ContinueDeployment",
38 "CreateApplication",
39 "CreateDeployment",
40 "CreateDeploymentConfig",
41 "CreateDeploymentGroup",
42 "DeleteApplication",
43 "DeleteDeploymentConfig",
44 "DeleteDeploymentGroup",
45 "DeleteGitHubAccountToken",
46 "DeleteResourcesByExternalId",
47 "DeregisterOnPremisesInstance",
48 "GetApplication",
49 "GetApplicationRevision",
50 "GetDeployment",
51 "GetDeploymentConfig",
52 "GetDeploymentGroup",
53 "GetDeploymentInstance",
54 "GetDeploymentTarget",
55 "GetOnPremisesInstance",
56 "ListApplicationRevisions",
57 "ListApplications",
58 "ListDeploymentConfigs",
59 "ListDeploymentGroups",
60 "ListDeploymentInstances",
61 "ListDeploymentTargets",
62 "ListDeployments",
63 "ListGitHubAccountTokenNames",
64 "ListOnPremisesInstances",
65 "ListTagsForResource",
66 "PutLifecycleEventHookExecutionStatus",
67 "RegisterApplicationRevision",
68 "RegisterOnPremisesInstance",
69 "RemoveTagsFromOnPremisesInstances",
70 "SkipWaitTimeForInstanceTermination",
71 "StopDeployment",
72 "TagResource",
73 "UntagResource",
74 "UpdateApplication",
75 "UpdateDeploymentGroup",
76];
77
78pub struct CodeDeployService {
79 state: SharedCodeDeployState,
80 snapshot_store: Option<Arc<dyn SnapshotStore>>,
81 snapshot_lock: Arc<AsyncMutex<()>>,
82}
83
84impl CodeDeployService {
85 pub fn new(state: SharedCodeDeployState) -> Self {
86 Self {
87 state,
88 snapshot_store: None,
89 snapshot_lock: Arc::new(AsyncMutex::new(())),
90 }
91 }
92
93 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
94 self.snapshot_store = Some(store);
95 self
96 }
97
98 async fn save(&self) {
99 crate::persistence::save_snapshot(
100 &self.state,
101 self.snapshot_store.clone(),
102 &self.snapshot_lock,
103 )
104 .await;
105 }
106
107 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
109 let store = self.snapshot_store.clone()?;
110 let state = self.state.clone();
111 let lock = self.snapshot_lock.clone();
112 Some(Arc::new(move || {
113 let state = state.clone();
114 let store = store.clone();
115 let lock = lock.clone();
116 Box::pin(async move {
117 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
118 })
119 }))
120 }
121}
122
123fn is_mutating(action: &str) -> bool {
127 matches!(
128 action,
129 "AddTagsToOnPremisesInstances"
130 | "BatchGetDeployments"
131 | "CreateApplication"
132 | "CreateDeployment"
133 | "CreateDeploymentConfig"
134 | "CreateDeploymentGroup"
135 | "DeleteApplication"
136 | "DeleteDeploymentConfig"
137 | "DeleteDeploymentGroup"
138 | "DeleteResourcesByExternalId"
139 | "DeregisterOnPremisesInstance"
140 | "GetDeployment"
141 | "RegisterApplicationRevision"
142 | "RegisterOnPremisesInstance"
143 | "RemoveTagsFromOnPremisesInstances"
144 | "StopDeployment"
145 | "TagResource"
146 | "UntagResource"
147 | "UpdateApplication"
148 | "UpdateDeploymentGroup"
149 )
150}
151
152#[async_trait]
153impl AwsService for CodeDeployService {
154 fn service_name(&self) -> &str {
155 "codedeploy"
156 }
157
158 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
159 let action = req.action.clone();
160 let result = self.dispatch(&action, &req);
161 if is_mutating(&action) && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
162 self.save().await;
163 }
164 result
165 }
166
167 fn supported_actions(&self) -> &[&str] {
168 CODEDEPLOY_ACTIONS
169 }
170}
171
172impl CodeDeployService {
173 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
174 match action {
175 "CreateApplication" => self.create_application(req),
176 "GetApplication" => self.get_application(req),
177 "UpdateApplication" => self.update_application(req),
178 "DeleteApplication" => self.delete_application(req),
179 "ListApplications" => self.list_applications(req),
180 "BatchGetApplications" => self.batch_get_applications(req),
181 "RegisterApplicationRevision" => self.register_application_revision(req),
182 "GetApplicationRevision" => self.get_application_revision(req),
183 "ListApplicationRevisions" => self.list_application_revisions(req),
184 "BatchGetApplicationRevisions" => self.batch_get_application_revisions(req),
185 "CreateDeploymentGroup" => self.create_deployment_group(req),
186 "GetDeploymentGroup" => self.get_deployment_group(req),
187 "UpdateDeploymentGroup" => self.update_deployment_group(req),
188 "DeleteDeploymentGroup" => self.delete_deployment_group(req),
189 "ListDeploymentGroups" => self.list_deployment_groups(req),
190 "BatchGetDeploymentGroups" => self.batch_get_deployment_groups(req),
191 "CreateDeploymentConfig" => self.create_deployment_config(req),
192 "GetDeploymentConfig" => self.get_deployment_config(req),
193 "DeleteDeploymentConfig" => self.delete_deployment_config(req),
194 "ListDeploymentConfigs" => self.list_deployment_configs(req),
195 "CreateDeployment" => self.create_deployment(req),
196 "GetDeployment" => self.get_deployment(req),
197 "BatchGetDeployments" => self.batch_get_deployments(req),
198 "ListDeployments" => self.list_deployments(req),
199 "StopDeployment" => self.stop_deployment(req),
200 "ContinueDeployment" => self.continue_deployment(req),
201 "SkipWaitTimeForInstanceTermination" => self.skip_wait_time(req),
202 "GetDeploymentTarget" => self.get_deployment_target(req),
203 "ListDeploymentTargets" => self.list_deployment_targets(req),
204 "BatchGetDeploymentTargets" => self.batch_get_deployment_targets(req),
205 "GetDeploymentInstance" => self.get_deployment_instance(req),
206 "ListDeploymentInstances" => self.list_deployment_instances(req),
207 "BatchGetDeploymentInstances" => self.batch_get_deployment_instances(req),
208 "RegisterOnPremisesInstance" => self.register_on_premises_instance(req),
209 "DeregisterOnPremisesInstance" => self.deregister_on_premises_instance(req),
210 "GetOnPremisesInstance" => self.get_on_premises_instance(req),
211 "ListOnPremisesInstances" => self.list_on_premises_instances(req),
212 "BatchGetOnPremisesInstances" => self.batch_get_on_premises_instances(req),
213 "AddTagsToOnPremisesInstances" => self.add_tags_to_on_premises(req),
214 "RemoveTagsFromOnPremisesInstances" => self.remove_tags_from_on_premises(req),
215 "ListGitHubAccountTokenNames" => self.list_github_account_token_names(req),
216 "DeleteGitHubAccountToken" => self.delete_github_account_token(req),
217 "PutLifecycleEventHookExecutionStatus" => self.put_lifecycle_status(req),
218 "DeleteResourcesByExternalId" => self.delete_resources_by_external_id(req),
219 "TagResource" => self.tag_resource(req),
220 "UntagResource" => self.untag_resource(req),
221 "ListTagsForResource" => self.list_tags_for_resource(req),
222 _ => Err(AwsServiceError::action_not_implemented(
223 self.service_name(),
224 action,
225 )),
226 }
227 }
228}
229
230fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
235 Ok(AwsResponse::json_value(StatusCode::OK, v))
236}
237
238fn ts(dt: DateTime<Utc>) -> Value {
239 json!(dt.timestamp_millis() as f64 / 1000.0)
240}
241
242fn e(code: &str, msg: impl Into<String>) -> AwsServiceError {
243 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
244}
245
246fn body(req: &AwsRequest) -> Value {
247 serde_json::from_slice(&req.body).unwrap_or(Value::Null)
248}
249
250fn str_field(b: &Value, key: &str) -> Option<String> {
251 b.get(key).and_then(|v| v.as_str()).map(str::to_string)
252}
253
254fn str_list(b: &Value, key: &str) -> Vec<String> {
255 b.get(key)
256 .and_then(|v| v.as_array())
257 .map(|a| {
258 a.iter()
259 .filter_map(|x| x.as_str().map(str::to_string))
260 .collect()
261 })
262 .unwrap_or_default()
263}
264
265fn len_ok(v: &str, min: usize, max: usize) -> bool {
266 let n = v.chars().count();
267 n >= min && n <= max
268}
269
270fn gen_deployment_id() -> String {
273 let raw = uuid::Uuid::new_v4().simple().to_string().to_uppercase();
274 let suffix: String = raw
275 .chars()
276 .filter(|c| c.is_ascii_alphanumeric())
277 .take(9)
278 .collect();
279 format!("d-{suffix}")
280}
281
282fn gen_uuid() -> String {
283 uuid::Uuid::new_v4().to_string()
284}
285
286fn req_application_name(b: &Value) -> Result<String, AwsServiceError> {
291 match str_field(b, "applicationName") {
292 None => Err(e(
293 "ApplicationNameRequiredException",
294 "The minimum number of required application inputs was not satisfied.",
295 )),
296 Some(v) if !len_ok(&v, 1, 100) => Err(e(
297 "InvalidApplicationNameException",
298 "The application name was specified in an invalid format.",
299 )),
300 Some(v) => Ok(v),
301 }
302}
303
304fn check_application_name(v: &str) -> Result<(), AwsServiceError> {
305 if len_ok(v, 1, 100) {
306 Ok(())
307 } else {
308 Err(e(
309 "InvalidApplicationNameException",
310 "The application name was specified in an invalid format.",
311 ))
312 }
313}
314
315fn req_deployment_group_name(b: &Value, key: &str) -> Result<String, AwsServiceError> {
316 match str_field(b, key) {
317 None => Err(e(
318 "DeploymentGroupNameRequiredException",
319 "The deployment group name was not specified.",
320 )),
321 Some(v) if !len_ok(&v, 1, 100) => Err(e(
322 "InvalidDeploymentGroupNameException",
323 "The deployment group name was specified in an invalid format.",
324 )),
325 Some(v) => Ok(v),
326 }
327}
328
329fn check_deployment_group_name(v: &str) -> Result<(), AwsServiceError> {
330 if len_ok(v, 1, 100) {
331 Ok(())
332 } else {
333 Err(e(
334 "InvalidDeploymentGroupNameException",
335 "The deployment group name was specified in an invalid format.",
336 ))
337 }
338}
339
340fn req_deployment_config_name(b: &Value) -> Result<String, AwsServiceError> {
341 match str_field(b, "deploymentConfigName") {
342 None => Err(e(
343 "DeploymentConfigNameRequiredException",
344 "The deployment configuration name was not specified.",
345 )),
346 Some(v) if !len_ok(&v, 1, 100) => Err(e(
347 "InvalidDeploymentConfigNameException",
348 "The deployment configuration name was specified in an invalid format.",
349 )),
350 Some(v) => Ok(v),
351 }
352}
353
354fn check_deployment_config_name(v: &str) -> Result<(), AwsServiceError> {
355 if len_ok(v, 1, 100) {
356 Ok(())
357 } else {
358 Err(e(
359 "InvalidDeploymentConfigNameException",
360 "The deployment configuration name was specified in an invalid format.",
361 ))
362 }
363}
364
365fn req_deployment_id(b: &Value) -> Result<String, AwsServiceError> {
366 match str_field(b, "deploymentId") {
367 None => Err(e(
368 "DeploymentIdRequiredException",
369 "The deployment ID was not specified.",
370 )),
371 Some(v) if v.is_empty() => Err(e(
372 "InvalidDeploymentIdException",
373 "The deployment ID was specified in an invalid format.",
374 )),
375 Some(v) => Ok(v),
376 }
377}
378
379fn req_instance_name(b: &Value) -> Result<String, AwsServiceError> {
380 match str_field(b, "instanceName") {
381 None => Err(e(
382 "InstanceNameRequiredException",
383 "An on-premises instance name was not specified.",
384 )),
385 Some(v) if v.is_empty() => Err(e(
386 "InvalidInstanceNameException",
387 "The on-premises instance name was specified in an invalid format.",
388 )),
389 Some(v) => Ok(v),
390 }
391}
392
393fn req_resource_arn(b: &Value) -> Result<String, AwsServiceError> {
394 match str_field(b, "ResourceArn") {
395 None => Err(e(
396 "ResourceArnRequiredException",
397 "The ARN of a resource is required, but was not specified.",
398 )),
399 Some(v) if !len_ok(&v, 1, 1011) || !is_codedeploy_arn(&v) => Err(e(
400 "InvalidArnException",
401 "The specified ARN is not in a valid format.",
402 )),
403 Some(v) => Ok(v),
404 }
405}
406
407fn is_codedeploy_arn(s: &str) -> bool {
410 let parts: Vec<&str> = s.splitn(7, ':').collect();
411 if parts.len() < 7 || parts[0] != "arn" || parts[2] != "codedeploy" {
412 return false;
413 }
414 matches!(
415 parts[5],
416 "application" | "deploymentgroup" | "deploymentconfig"
417 )
418}
419
420fn application_arn(region: &str, account: &str, name: &str) -> String {
421 format!("arn:aws:codedeploy:{region}:{account}:application:{name}")
422}
423
424fn deployment_group_arn(region: &str, account: &str, app: &str, group: &str) -> String {
425 format!("arn:aws:codedeploy:{region}:{account}:deploymentgroup:{app}/{group}")
426}
427
428fn deployment_config_arn(region: &str, account: &str, name: &str) -> String {
429 format!("arn:aws:codedeploy:{region}:{account}:deploymentconfig:{name}")
430}
431
432fn opt_enum(
433 b: &Value,
434 key: &str,
435 set: &[&str],
436 code: &str,
437 msg: &str,
438) -> Result<(), AwsServiceError> {
439 if let Some(v) = str_field(b, key) {
440 if !validate::is_enum(set, &v) {
441 return Err(e(code, msg));
442 }
443 }
444 Ok(())
445}
446
447fn merge_tags(stored: &mut Vec<Value>, incoming: &[Value]) {
450 for t in incoming {
451 if let Some(k) = t.get("Key").and_then(Value::as_str).map(str::to_string) {
452 stored.retain(|x| x.get("Key").and_then(Value::as_str) != Some(k.as_str()));
453 }
454 stored.push(t.clone());
455 }
456}
457
458fn validate_tag_filters(
461 b: &Value,
462 key: &str,
463 err_code: &str,
464 msg: &str,
465) -> Result<(), AwsServiceError> {
466 if let Some(filters) = b.get(key).and_then(Value::as_array) {
467 for f in filters {
468 if let Some(t) = f.get("Type").and_then(Value::as_str) {
469 if !validate::is_enum(validate::TAG_FILTER_TYPE, t) {
470 return Err(e(err_code, msg));
471 }
472 }
473 }
474 }
475 Ok(())
476}
477
478fn validate_deployment_group_config(b: &Value) -> Result<(), AwsServiceError> {
483 validate_tag_filters(
484 b,
485 "ec2TagFilters",
486 "InvalidEC2TagException",
487 "The EC2 tag was specified in an invalid format.",
488 )?;
489 validate_tag_filters(
490 b,
491 "onPremisesInstanceTagFilters",
492 "InvalidOnPremisesTagCombinationException",
493 "The on-premises instance tag was specified in an invalid format.",
494 )?;
495 if let Some(triggers) = b.get("triggerConfigurations").and_then(Value::as_array) {
496 for tc in triggers {
497 if let Some(events) = tc.get("triggerEvents").and_then(Value::as_array) {
498 for ev in events.iter().filter_map(Value::as_str) {
499 if !validate::is_enum(validate::TRIGGER_EVENT_TYPE, ev) {
500 return Err(e(
501 "InvalidTriggerConfigException",
502 "The trigger was specified in an invalid format.",
503 ));
504 }
505 }
506 }
507 }
508 }
509 if let Some(events) = b
510 .get("autoRollbackConfiguration")
511 .and_then(|c| c.get("events"))
512 .and_then(Value::as_array)
513 {
514 for ev in events.iter().filter_map(Value::as_str) {
515 if !validate::is_enum(validate::AUTO_ROLLBACK_EVENT, ev) {
516 return Err(e(
517 "InvalidAutoRollbackConfigException",
518 "The automatic rollback configuration was specified in an invalid format.",
519 ));
520 }
521 }
522 }
523 Ok(())
524}
525
526fn validate_minimum_healthy_hosts(b: &Value) -> Result<(), AwsServiceError> {
532 let Some(mhh) = b.get("minimumHealthyHosts").filter(|v| !v.is_null()) else {
533 return Ok(());
534 };
535 let bad = || {
536 e(
537 "InvalidMinimumHealthyHostValueException",
538 "The minimum healthy instances value was specified in an invalid format.",
539 )
540 };
541 let mhh_type = mhh.get("type").and_then(Value::as_str);
542 if let Some(t) = mhh_type {
543 if !validate::is_enum(validate::MINIMUM_HEALTHY_HOSTS_TYPE, t) {
544 return Err(bad());
545 }
546 }
547 if let Some(value) = mhh.get("value").and_then(Value::as_i64) {
548 let out_of_range = match mhh_type {
549 Some("FLEET_PERCENT") => !(0..=100).contains(&value),
550 _ => value < 0,
551 };
552 if out_of_range {
553 return Err(bad());
554 }
555 }
556 Ok(())
557}
558
559fn paginate_body(items: Vec<Value>, list_key: &str, b: &Value) -> Result<Value, AwsServiceError> {
563 let token = str_field(b, "nextToken");
564 let (page, next) = paginate_checked(&items, token.as_deref(), 100).map_err(|_| {
565 e(
566 "InvalidNextTokenException",
567 "The next token was specified in an invalid format.",
568 )
569 })?;
570 let mut out = Map::new();
571 out.insert(list_key.to_string(), Value::Array(page));
572 if let Some(n) = next {
573 out.insert("nextToken".to_string(), Value::String(n));
574 }
575 Ok(Value::Object(out))
576}
577
578impl CodeDeployService {
579 fn region_account(&self, req: &AwsRequest) -> (String, String) {
580 (req.region.clone(), req.account_id.clone())
581 }
582}
583
584fn predefined_config(name: &str) -> Option<Value> {
591 let server = |mhh_type: &str, value: i64| {
592 json!({
593 "deploymentConfigId": name,
594 "deploymentConfigName": name,
595 "computePlatform": "Server",
596 "minimumHealthyHosts": { "type": mhh_type, "value": value },
597 })
598 };
599 let canary = |platform: &str, pct: i64, mins: i64| {
600 json!({
601 "deploymentConfigId": name,
602 "deploymentConfigName": name,
603 "computePlatform": platform,
604 "trafficRoutingConfig": {
605 "type": "TimeBasedCanary",
606 "timeBasedCanary": { "canaryPercentage": pct, "canaryInterval": mins },
607 },
608 })
609 };
610 let linear = |platform: &str, pct: i64, mins: i64| {
611 json!({
612 "deploymentConfigId": name,
613 "deploymentConfigName": name,
614 "computePlatform": platform,
615 "trafficRoutingConfig": {
616 "type": "TimeBasedLinear",
617 "timeBasedLinear": { "linearPercentage": pct, "linearInterval": mins },
618 },
619 })
620 };
621 let all_at_once = |platform: &str| {
622 json!({
623 "deploymentConfigId": name,
624 "deploymentConfigName": name,
625 "computePlatform": platform,
626 "trafficRoutingConfig": { "type": "AllAtOnce" },
627 })
628 };
629 match name {
630 "CodeDeployDefault.OneAtATime" => Some(server("HOST_COUNT", 99)),
632 "CodeDeployDefault.HalfAtATime" => Some(server("FLEET_PERCENT", 50)),
633 "CodeDeployDefault.AllAtOnce" => Some(server("HOST_COUNT", 0)),
634 "CodeDeployDefault.LambdaAllAtOnce" => Some(all_at_once("Lambda")),
636 "CodeDeployDefault.LambdaCanary10Percent5Minutes" => Some(canary("Lambda", 10, 5)),
637 "CodeDeployDefault.LambdaCanary10Percent10Minutes" => Some(canary("Lambda", 10, 10)),
638 "CodeDeployDefault.LambdaCanary10Percent15Minutes" => Some(canary("Lambda", 10, 15)),
639 "CodeDeployDefault.LambdaCanary10Percent30Minutes" => Some(canary("Lambda", 10, 30)),
640 "CodeDeployDefault.LambdaLinear10PercentEvery1Minute" => Some(linear("Lambda", 10, 1)),
641 "CodeDeployDefault.LambdaLinear10PercentEvery2Minutes" => Some(linear("Lambda", 10, 2)),
642 "CodeDeployDefault.LambdaLinear10PercentEvery3Minutes" => Some(linear("Lambda", 10, 3)),
643 "CodeDeployDefault.LambdaLinear10PercentEvery10Minutes" => Some(linear("Lambda", 10, 10)),
644 "CodeDeployDefault.ECSAllAtOnce" => Some(all_at_once("ECS")),
646 "CodeDeployDefault.ECSCanary10Percent5Minutes" => Some(canary("ECS", 10, 5)),
647 "CodeDeployDefault.ECSCanary10Percent15Minutes" => Some(canary("ECS", 10, 15)),
648 "CodeDeployDefault.ECSLinear10PercentEvery1Minutes" => Some(linear("ECS", 10, 1)),
649 "CodeDeployDefault.ECSLinear10PercentEvery3Minutes" => Some(linear("ECS", 10, 3)),
650 _ => None,
651 }
652}
653
654const PREDEFINED_CONFIG_NAMES: &[&str] = &[
655 "CodeDeployDefault.OneAtATime",
656 "CodeDeployDefault.HalfAtATime",
657 "CodeDeployDefault.AllAtOnce",
658 "CodeDeployDefault.LambdaAllAtOnce",
659 "CodeDeployDefault.LambdaCanary10Percent5Minutes",
660 "CodeDeployDefault.LambdaCanary10Percent10Minutes",
661 "CodeDeployDefault.LambdaCanary10Percent15Minutes",
662 "CodeDeployDefault.LambdaCanary10Percent30Minutes",
663 "CodeDeployDefault.LambdaLinear10PercentEvery1Minute",
664 "CodeDeployDefault.LambdaLinear10PercentEvery2Minutes",
665 "CodeDeployDefault.LambdaLinear10PercentEvery3Minutes",
666 "CodeDeployDefault.LambdaLinear10PercentEvery10Minutes",
667 "CodeDeployDefault.ECSAllAtOnce",
668 "CodeDeployDefault.ECSCanary10Percent5Minutes",
669 "CodeDeployDefault.ECSCanary10Percent15Minutes",
670 "CodeDeployDefault.ECSLinear10PercentEvery1Minutes",
671 "CodeDeployDefault.ECSLinear10PercentEvery3Minutes",
672];
673
674impl CodeDeployService {
679 fn create_application(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
680 let b = body(req);
681 let name = req_application_name(&b)?;
682 opt_enum(
683 &b,
684 "computePlatform",
685 validate::COMPUTE_PLATFORM,
686 "InvalidComputePlatformException",
687 "The computePlatform is invalid.",
688 )?;
689 let (region, account) = self.region_account(req);
690 let compute = str_field(&b, "computePlatform").unwrap_or_else(|| "Server".into());
691 let app_id = gen_uuid();
692 let now = Utc::now();
693 let mut guard = self.state.write();
694 let st = guard.get_or_create(&account);
695 if st.applications.contains_key(&name) {
696 return Err(e(
697 "ApplicationAlreadyExistsException",
698 format!("An application already exists with the name {name}."),
699 ));
700 }
701 let info = json!({
702 "applicationId": app_id,
703 "applicationName": name,
704 "createTime": ts(now),
705 "linkedToGitHub": false,
706 "computePlatform": compute,
707 });
708 st.applications.insert(name.clone(), info);
709 st.application_order.push(name.clone());
710 if let Some(tags) = b.get("tags").and_then(Value::as_array) {
713 if !tags.is_empty() {
714 let arn = application_arn(®ion, &account, &name);
715 merge_tags(st.tags.entry(arn).or_default(), tags);
716 }
717 }
718 ok(json!({ "applicationId": app_id }))
719 }
720
721 fn get_application(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
722 let b = body(req);
723 let name = req_application_name(&b)?;
724 let (_region, account) = self.region_account(req);
725 let mut guard = self.state.write();
726 let st = guard.get_or_create(&account);
727 let Some(info) = st.applications.get(&name) else {
728 return Err(e(
729 "ApplicationDoesNotExistException",
730 format!("No application found for name: {name}"),
731 ));
732 };
733 ok(json!({ "application": info.clone() }))
734 }
735
736 fn update_application(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
737 let b = body(req);
738 if let Some(v) = str_field(&b, "applicationName") {
740 check_application_name(&v)?;
741 }
742 if let Some(v) = str_field(&b, "newApplicationName") {
743 check_application_name(&v)?;
744 }
745 let Some(current) = str_field(&b, "applicationName") else {
746 return Err(e(
747 "ApplicationNameRequiredException",
748 "The minimum number of required application inputs was not satisfied.",
749 ));
750 };
751 let (_region, account) = self.region_account(req);
752 let mut guard = self.state.write();
753 let st = guard.get_or_create(&account);
754 if !st.applications.contains_key(¤t) {
755 return Err(e(
756 "ApplicationDoesNotExistException",
757 format!("No application found for name: {current}"),
758 ));
759 }
760 if let Some(new_name) = str_field(&b, "newApplicationName") {
761 if new_name != current && st.applications.contains_key(&new_name) {
762 return Err(e(
763 "ApplicationAlreadyExistsException",
764 format!("An application already exists with the name {new_name}."),
765 ));
766 }
767 if new_name != current {
768 let mut info = st.applications.remove(¤t).unwrap();
769 if let Some(obj) = info.as_object_mut() {
770 obj.insert("applicationName".into(), json!(new_name));
771 }
772 st.applications.insert(new_name.clone(), info);
773 for n in st.application_order.iter_mut() {
774 if *n == current {
775 *n = new_name.clone();
776 }
777 }
778 if let Some(revs) = st.app_revisions.remove(¤t) {
779 st.app_revisions.insert(new_name.clone(), revs);
780 }
781 if let Some(mut groups) = st.deployment_groups.remove(¤t) {
785 for group in groups.values_mut() {
786 if let Some(obj) = group.as_object_mut() {
787 obj.insert("applicationName".into(), json!(new_name));
788 }
789 }
790 st.deployment_groups.insert(new_name.clone(), groups);
791 }
792 for deployment in st.deployments.values_mut() {
796 if deployment.get("applicationName").and_then(Value::as_str)
797 == Some(current.as_str())
798 {
799 if let Some(obj) = deployment.as_object_mut() {
800 obj.insert("applicationName".into(), json!(new_name));
801 }
802 }
803 }
804 }
805 }
806 ok(json!({}))
807 }
808
809 fn delete_application(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
810 let b = body(req);
811 let name = req_application_name(&b)?;
812 let (region, account) = self.region_account(req);
813 let mut guard = self.state.write();
814 let st = guard.get_or_create(&account);
815 st.applications.remove(&name);
818 st.application_order.retain(|n| n != &name);
819 st.app_revisions.remove(&name);
820 st.tags.remove(&application_arn(®ion, &account, &name));
823 if let Some(groups) = st.deployment_groups.remove(&name) {
824 for dg in groups.keys() {
825 st.tags
826 .remove(&deployment_group_arn(®ion, &account, &name, dg));
827 }
828 }
829 ok(json!({}))
830 }
831
832 fn list_applications(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
833 let b = body(req);
834 let (_region, account) = self.region_account(req);
835 let mut guard = self.state.write();
836 let st = guard.get_or_create(&account);
837 let names: Vec<Value> = st.application_order.iter().map(|n| json!(n)).collect();
838 ok(paginate_body(names, "applications", &b)?)
839 }
840
841 fn batch_get_applications(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
842 let b = body(req);
843 let names = str_list(&b, "applicationNames");
844 let (_region, account) = self.region_account(req);
845 let mut guard = self.state.write();
846 let st = guard.get_or_create(&account);
847 let mut infos = Vec::with_capacity(names.len());
851 for n in &names {
852 match st.applications.get(n) {
853 Some(info) => infos.push(info.clone()),
854 None => {
855 return Err(e(
856 "ApplicationDoesNotExistException",
857 format!("No application found for name: {n}"),
858 ))
859 }
860 }
861 }
862 ok(json!({ "applicationsInfo": infos }))
863 }
864}
865
866fn validate_revision(rev: &Value) -> Result<(), AwsServiceError> {
872 if let Some(t) = rev.get("revisionType").and_then(Value::as_str) {
873 if !validate::is_enum(validate::REVISION_LOCATION_TYPE, t) {
874 return Err(e(
875 "InvalidRevisionException",
876 "The revision was specified in an invalid format.",
877 ));
878 }
879 }
880 Ok(())
881}
882
883impl CodeDeployService {
884 fn register_application_revision(
885 &self,
886 req: &AwsRequest,
887 ) -> Result<AwsResponse, AwsServiceError> {
888 let b = body(req);
889 let name = req_application_name(&b)?;
890 let Some(revision) = b.get("revision").filter(|v| !v.is_null()).cloned() else {
891 return Err(e(
892 "RevisionRequiredException",
893 "The revision ID was not specified.",
894 ));
895 };
896 validate_revision(&revision)?;
897 let now = Utc::now();
898 let (_region, account) = self.region_account(req);
899 let mut guard = self.state.write();
900 let st = guard.get_or_create(&account);
901 if !st.applications.contains_key(&name) {
902 return Err(e(
903 "ApplicationDoesNotExistException",
904 format!("No application found for name: {name}"),
905 ));
906 }
907 let description = str_field(&b, "description");
908 let revs = st.app_revisions.entry(name).or_default();
909 if let Some(existing) = revs
912 .iter_mut()
913 .find(|r| r.get("revisionLocation") == Some(&revision))
914 {
915 if let Some(obj) = existing.as_object_mut() {
916 let generic = obj
917 .entry("genericRevisionInfo")
918 .or_insert_with(|| json!({}));
919 if let Some(g) = generic.as_object_mut() {
920 g.insert("lastUsedTime".into(), ts(now));
921 if let Some(d) = &description {
922 g.insert("description".into(), json!(d));
923 }
924 }
925 }
926 } else {
927 let mut generic = Map::new();
928 generic.insert("registerTime".into(), ts(now));
929 generic.insert("firstUsedTime".into(), ts(now));
930 generic.insert("lastUsedTime".into(), ts(now));
931 generic.insert("deploymentGroups".into(), json!([]));
932 if let Some(d) = &description {
933 generic.insert("description".into(), json!(d));
934 }
935 revs.push(json!({
936 "revisionLocation": revision,
937 "genericRevisionInfo": Value::Object(generic),
938 }));
939 }
940 ok(json!({}))
941 }
942
943 fn get_application_revision(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
944 let b = body(req);
945 let name = req_application_name(&b)?;
946 let Some(revision) = b.get("revision").filter(|v| !v.is_null()).cloned() else {
947 return Err(e(
948 "RevisionRequiredException",
949 "The revision ID was not specified.",
950 ));
951 };
952 validate_revision(&revision)?;
953 let (_region, account) = self.region_account(req);
954 let mut guard = self.state.write();
955 let st = guard.get_or_create(&account);
956 if !st.applications.contains_key(&name) {
957 return Err(e(
958 "ApplicationDoesNotExistException",
959 format!("No application found for name: {name}"),
960 ));
961 }
962 let record = st.app_revisions.get(&name).and_then(|revs| {
963 revs.iter()
964 .find(|r| r.get("revisionLocation") == Some(&revision))
965 });
966 let Some(record) = record else {
967 return Err(e(
968 "RevisionDoesNotExistException",
969 "The named revision does not exist with the applicable IAM user or AWS account.",
970 ));
971 };
972 ok(json!({
973 "applicationName": name,
974 "revision": revision,
975 "revisionInfo": record.get("genericRevisionInfo").cloned().unwrap_or_else(|| json!({})),
976 }))
977 }
978
979 fn list_application_revisions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
980 let b = body(req);
981 let name = req_application_name(&b)?;
982 opt_enum(&b, "sortBy", validate::APPLICATION_REVISION_SORT_BY, "InvalidSortByException", "The column name to sort by is either not present or was specified in an invalid format.")?;
983 opt_enum(
984 &b,
985 "sortOrder",
986 validate::SORT_ORDER,
987 "InvalidSortOrderException",
988 "The sort order was specified in an invalid format.",
989 )?;
990 opt_enum(
991 &b,
992 "deployed",
993 validate::LIST_STATE_FILTER_ACTION,
994 "InvalidDeployedStateFilterException",
995 "The deployed state filter was specified in an invalid format.",
996 )?;
997 let (_region, account) = self.region_account(req);
998 let mut guard = self.state.write();
999 let st = guard.get_or_create(&account);
1000 if !st.applications.contains_key(&name) {
1001 return Err(e(
1002 "ApplicationDoesNotExistException",
1003 format!("No application found for name: {name}"),
1004 ));
1005 }
1006 let is_deployed = |r: &Value| -> bool {
1009 r.get("genericRevisionInfo")
1010 .and_then(|g| g.get("deploymentGroups"))
1011 .and_then(Value::as_array)
1012 .map(|a| !a.is_empty())
1013 .unwrap_or(false)
1014 };
1015 let deployed_filter = str_field(&b, "deployed").unwrap_or_else(|| "ignore".into());
1016 let mut records: Vec<Value> = st
1017 .app_revisions
1018 .get(&name)
1019 .map(|revs| {
1020 revs.iter()
1021 .filter(|r| match deployed_filter.as_str() {
1022 "include" => is_deployed(r),
1023 "exclude" => !is_deployed(r),
1024 _ => true,
1025 })
1026 .cloned()
1027 .collect()
1028 })
1029 .unwrap_or_default();
1030 if let Some(sort_by) = str_field(&b, "sortBy") {
1033 let key = |r: &Value| -> f64 {
1034 r.get("genericRevisionInfo")
1035 .and_then(|g| g.get(sort_by.as_str()))
1036 .and_then(Value::as_f64)
1037 .unwrap_or(0.0)
1038 };
1039 records.sort_by(|x, y| {
1040 key(x)
1041 .partial_cmp(&key(y))
1042 .unwrap_or(std::cmp::Ordering::Equal)
1043 });
1044 if str_field(&b, "sortOrder").as_deref() == Some("descending") {
1045 records.reverse();
1046 }
1047 }
1048 let revisions: Vec<Value> = records
1049 .iter()
1050 .filter_map(|r| r.get("revisionLocation").cloned())
1051 .collect();
1052 ok(paginate_body(revisions, "revisions", &b)?)
1053 }
1054
1055 fn batch_get_application_revisions(
1056 &self,
1057 req: &AwsRequest,
1058 ) -> Result<AwsResponse, AwsServiceError> {
1059 let b = body(req);
1060 let name = req_application_name(&b)?;
1061 let requested = b
1062 .get("revisions")
1063 .and_then(Value::as_array)
1064 .cloned()
1065 .unwrap_or_default();
1066 let (_region, account) = self.region_account(req);
1067 let mut guard = self.state.write();
1068 let st = guard.get_or_create(&account);
1069 if !st.applications.contains_key(&name) {
1070 return Err(e(
1071 "ApplicationDoesNotExistException",
1072 format!("No application found for name: {name}"),
1073 ));
1074 }
1075 let stored = st.app_revisions.get(&name);
1076 let infos: Vec<Value> = requested
1077 .into_iter()
1078 .map(|loc| {
1079 let generic = stored
1080 .and_then(|revs| {
1081 revs.iter()
1082 .find(|r| r.get("revisionLocation") == Some(&loc))
1083 })
1084 .and_then(|r| r.get("genericRevisionInfo").cloned())
1085 .unwrap_or_else(|| json!({}));
1086 json!({ "revisionLocation": loc, "genericRevisionInfo": generic })
1087 })
1088 .collect();
1089 ok(json!({ "applicationName": name, "revisions": infos }))
1090 }
1091}
1092
1093struct DeploymentGroupIdent<'a> {
1099 app: &'a str,
1100 dg_name: &'a str,
1101 dg_id: &'a str,
1102 service_role: &'a str,
1103 config_name: &'a str,
1104 compute: &'a str,
1105}
1106
1107impl CodeDeployService {
1108 fn app_compute_platform(st: &CodeDeployState, app: &str) -> String {
1110 st.applications
1111 .get(app)
1112 .and_then(|a| a.get("computePlatform").and_then(Value::as_str))
1113 .unwrap_or("Server")
1114 .to_string()
1115 }
1116
1117 fn assemble_deployment_group(&self, b: &Value, ident: &DeploymentGroupIdent) -> Value {
1120 let mut g = Map::new();
1121 g.insert("applicationName".into(), json!(ident.app));
1122 g.insert("deploymentGroupId".into(), json!(ident.dg_id));
1123 g.insert("deploymentGroupName".into(), json!(ident.dg_name));
1124 g.insert("deploymentConfigName".into(), json!(ident.config_name));
1125 g.insert("computePlatform".into(), json!(ident.compute));
1126 g.insert("serviceRoleArn".into(), json!(ident.service_role));
1127 for k in [
1128 "ec2TagFilters",
1129 "onPremisesInstanceTagFilters",
1130 "autoScalingGroups",
1131 "triggerConfigurations",
1132 "alarmConfiguration",
1133 "autoRollbackConfiguration",
1134 "deploymentStyle",
1135 "outdatedInstancesStrategy",
1136 "blueGreenDeploymentConfiguration",
1137 "loadBalancerInfo",
1138 "ec2TagSet",
1139 "onPremisesTagSet",
1140 "ecsServices",
1141 "terminationHookEnabled",
1142 ] {
1143 if let Some(v) = b.get(k).filter(|v| !v.is_null()) {
1144 g.insert(k.into(), v.clone());
1145 }
1146 }
1147 Value::Object(g)
1148 }
1149
1150 fn create_deployment_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1151 let b = body(req);
1152 let app = req_application_name(&b)?;
1153 let dg_name = req_deployment_group_name(&b, "deploymentGroupName")?;
1154 if let Some(v) = str_field(&b, "deploymentConfigName") {
1155 check_deployment_config_name(&v)?;
1156 }
1157 opt_enum(
1158 &b,
1159 "outdatedInstancesStrategy",
1160 validate::OUTDATED_INSTANCES_STRATEGY,
1161 "InvalidInputException",
1162 "The outdated instances strategy is invalid.",
1163 )?;
1164 validate_deployment_group_config(&b)?;
1165 let Some(service_role) = str_field(&b, "serviceRoleArn") else {
1166 return Err(e("RoleRequiredException", "The role ID was not specified."));
1167 };
1168 let (region, account) = self.region_account(req);
1169 let config_name = str_field(&b, "deploymentConfigName")
1170 .unwrap_or_else(|| "CodeDeployDefault.OneAtATime".into());
1171 let dg_id = gen_uuid();
1172 let mut guard = self.state.write();
1173 let st = guard.get_or_create(&account);
1174 if !st.applications.contains_key(&app) {
1175 return Err(e(
1176 "ApplicationDoesNotExistException",
1177 format!("No application found for name: {app}"),
1178 ));
1179 }
1180 if predefined_config(&config_name).is_none()
1181 && !st.deployment_configs.contains_key(&config_name)
1182 {
1183 return Err(e(
1184 "DeploymentConfigDoesNotExistException",
1185 format!("No deployment configuration found for name: {config_name}"),
1186 ));
1187 }
1188 let groups = st.deployment_groups.entry(app.clone()).or_default();
1189 if groups.contains_key(&dg_name) {
1190 return Err(e(
1191 "DeploymentGroupAlreadyExistsException",
1192 format!("A deployment group with the name {dg_name} already exists."),
1193 ));
1194 }
1195 let compute = Self::app_compute_platform(st, &app);
1196 let group = self.assemble_deployment_group(
1197 &b,
1198 &DeploymentGroupIdent {
1199 app: &app,
1200 dg_name: &dg_name,
1201 dg_id: &dg_id,
1202 service_role: &service_role,
1203 config_name: &config_name,
1204 compute: &compute,
1205 },
1206 );
1207 st.deployment_groups
1208 .entry(app.clone())
1209 .or_default()
1210 .insert(dg_name.clone(), group);
1211 if let Some(tags) = b.get("tags").and_then(Value::as_array) {
1214 if !tags.is_empty() {
1215 let arn = deployment_group_arn(®ion, &account, &app, &dg_name);
1216 merge_tags(st.tags.entry(arn).or_default(), tags);
1217 }
1218 }
1219 ok(json!({ "deploymentGroupId": dg_id }))
1220 }
1221
1222 fn get_deployment_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1223 let b = body(req);
1224 let app = req_application_name(&b)?;
1225 let dg_name = req_deployment_group_name(&b, "deploymentGroupName")?;
1226 let (_region, account) = self.region_account(req);
1227 let mut guard = self.state.write();
1228 let st = guard.get_or_create(&account);
1229 if !st.applications.contains_key(&app) {
1230 return Err(e(
1231 "ApplicationDoesNotExistException",
1232 format!("No application found for name: {app}"),
1233 ));
1234 }
1235 let Some(group) = st.deployment_groups.get(&app).and_then(|g| g.get(&dg_name)) else {
1236 return Err(e(
1237 "DeploymentGroupDoesNotExistException",
1238 format!("No deployment group found for name: {dg_name}"),
1239 ));
1240 };
1241 ok(json!({ "deploymentGroupInfo": group.clone() }))
1242 }
1243
1244 fn update_deployment_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1245 let b = body(req);
1246 let app = req_application_name(&b)?;
1247 let current = req_deployment_group_name(&b, "currentDeploymentGroupName")?;
1248 if let Some(v) = str_field(&b, "newDeploymentGroupName") {
1249 check_deployment_group_name(&v)?;
1250 }
1251 if let Some(v) = str_field(&b, "deploymentConfigName") {
1252 check_deployment_config_name(&v)?;
1253 }
1254 opt_enum(
1255 &b,
1256 "outdatedInstancesStrategy",
1257 validate::OUTDATED_INSTANCES_STRATEGY,
1258 "InvalidInputException",
1259 "The outdated instances strategy is invalid.",
1260 )?;
1261 validate_deployment_group_config(&b)?;
1262 let (_region, account) = self.region_account(req);
1263 let mut guard = self.state.write();
1264 let st = guard.get_or_create(&account);
1265 if !st.applications.contains_key(&app) {
1266 return Err(e(
1267 "ApplicationDoesNotExistException",
1268 format!("No application found for name: {app}"),
1269 ));
1270 }
1271 let new_name = str_field(&b, "newDeploymentGroupName");
1272 if let Some(new) = &new_name {
1273 if new != ¤t
1274 && st
1275 .deployment_groups
1276 .get(&app)
1277 .map(|g| g.contains_key(new))
1278 .unwrap_or(false)
1279 {
1280 return Err(e(
1281 "DeploymentGroupAlreadyExistsException",
1282 format!("A deployment group with the name {new} already exists."),
1283 ));
1284 }
1285 }
1286 let Some(existing) = st
1287 .deployment_groups
1288 .get(&app)
1289 .and_then(|g| g.get(¤t))
1290 .cloned()
1291 else {
1292 return Err(e(
1293 "DeploymentGroupDoesNotExistException",
1294 format!("No deployment group found for name: {current}"),
1295 ));
1296 };
1297 let mut merged = existing;
1298 if let Some(obj) = merged.as_object_mut() {
1299 for k in [
1300 "deploymentConfigName",
1301 "ec2TagFilters",
1302 "onPremisesInstanceTagFilters",
1303 "autoScalingGroups",
1304 "serviceRoleArn",
1305 "triggerConfigurations",
1306 "alarmConfiguration",
1307 "autoRollbackConfiguration",
1308 "deploymentStyle",
1309 "outdatedInstancesStrategy",
1310 "blueGreenDeploymentConfiguration",
1311 "loadBalancerInfo",
1312 "ec2TagSet",
1313 "onPremisesTagSet",
1314 "ecsServices",
1315 "terminationHookEnabled",
1316 ] {
1317 if let Some(v) = b.get(k).filter(|v| !v.is_null()) {
1318 obj.insert(k.into(), v.clone());
1319 }
1320 }
1321 if let Some(new) = &new_name {
1322 obj.insert("deploymentGroupName".into(), json!(new));
1323 }
1324 }
1325 let groups = st.deployment_groups.entry(app).or_default();
1326 groups.remove(¤t);
1327 let final_name = new_name.unwrap_or(current);
1328 groups.insert(final_name, merged);
1329 ok(json!({ "hooksNotCleanedUp": [] }))
1330 }
1331
1332 fn delete_deployment_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1333 let b = body(req);
1334 let app = req_application_name(&b)?;
1335 let dg_name = req_deployment_group_name(&b, "deploymentGroupName")?;
1336 let (region, account) = self.region_account(req);
1337 let mut guard = self.state.write();
1338 let st = guard.get_or_create(&account);
1339 if let Some(groups) = st.deployment_groups.get_mut(&app) {
1341 groups.remove(&dg_name);
1342 }
1343 st.tags
1345 .remove(&deployment_group_arn(®ion, &account, &app, &dg_name));
1346 ok(json!({ "hooksNotCleanedUp": [] }))
1347 }
1348
1349 fn list_deployment_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1350 let b = body(req);
1351 let app = req_application_name(&b)?;
1352 let (_region, account) = self.region_account(req);
1353 let mut guard = self.state.write();
1354 let st = guard.get_or_create(&account);
1355 if !st.applications.contains_key(&app) {
1356 return Err(e(
1357 "ApplicationDoesNotExistException",
1358 format!("No application found for name: {app}"),
1359 ));
1360 }
1361 let names: Vec<Value> = st
1362 .deployment_groups
1363 .get(&app)
1364 .map(|g| g.keys().map(|k| json!(k)).collect())
1365 .unwrap_or_default();
1366 let out = paginate_body(names, "deploymentGroups", &b)?;
1367 let mut out = out.as_object().cloned().unwrap_or_default();
1368 out.insert("applicationName".into(), json!(app));
1369 ok(Value::Object(out))
1370 }
1371
1372 fn batch_get_deployment_groups(
1373 &self,
1374 req: &AwsRequest,
1375 ) -> Result<AwsResponse, AwsServiceError> {
1376 let b = body(req);
1377 let app = req_application_name(&b)?;
1378 let names = str_list(&b, "deploymentGroupNames");
1379 let (_region, account) = self.region_account(req);
1380 let mut guard = self.state.write();
1381 let st = guard.get_or_create(&account);
1382 if !st.applications.contains_key(&app) {
1383 return Err(e(
1384 "ApplicationDoesNotExistException",
1385 format!("No application found for name: {app}"),
1386 ));
1387 }
1388 let groups = st.deployment_groups.get(&app);
1389 let infos: Vec<Value> = names
1390 .iter()
1391 .filter_map(|n| groups.and_then(|g| g.get(n)).cloned())
1392 .collect();
1393 ok(json!({ "deploymentGroupsInfo": infos }))
1394 }
1395}
1396
1397impl CodeDeployService {
1402 fn create_deployment_config(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1403 let b = body(req);
1404 let name = req_deployment_config_name(&b)?;
1405 opt_enum(
1406 &b,
1407 "computePlatform",
1408 validate::COMPUTE_PLATFORM,
1409 "InvalidComputePlatformException",
1410 "The computePlatform is invalid.",
1411 )?;
1412 validate_minimum_healthy_hosts(&b)?;
1413 let (_region, account) = self.region_account(req);
1414 let compute = str_field(&b, "computePlatform").unwrap_or_else(|| "Server".into());
1415 let config_id = gen_uuid();
1416 let now = Utc::now();
1417 let mut guard = self.state.write();
1418 let st = guard.get_or_create(&account);
1419 if predefined_config(&name).is_some() || st.deployment_configs.contains_key(&name) {
1420 return Err(e(
1421 "DeploymentConfigAlreadyExistsException",
1422 format!("A deployment configuration already exists with the name {name}."),
1423 ));
1424 }
1425 let mut info = Map::new();
1426 info.insert("deploymentConfigId".into(), json!(config_id));
1427 info.insert("deploymentConfigName".into(), json!(name));
1428 info.insert("computePlatform".into(), json!(compute));
1429 info.insert("createTime".into(), ts(now));
1430 for k in ["minimumHealthyHosts", "trafficRoutingConfig", "zonalConfig"] {
1431 if let Some(v) = b.get(k).filter(|v| !v.is_null()) {
1432 info.insert(k.into(), v.clone());
1433 }
1434 }
1435 st.deployment_configs
1436 .insert(name.clone(), Value::Object(info));
1437 st.deployment_config_order.push(name);
1438 ok(json!({ "deploymentConfigId": config_id }))
1439 }
1440
1441 fn get_deployment_config(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1442 let b = body(req);
1443 let name = req_deployment_config_name(&b)?;
1444 if let Some(info) = predefined_config(&name) {
1445 return ok(json!({ "deploymentConfigInfo": info }));
1446 }
1447 let (_region, account) = self.region_account(req);
1448 let mut guard = self.state.write();
1449 let st = guard.get_or_create(&account);
1450 let Some(info) = st.deployment_configs.get(&name) else {
1451 return Err(e(
1452 "DeploymentConfigDoesNotExistException",
1453 format!("No deployment configuration found for name: {name}"),
1454 ));
1455 };
1456 ok(json!({ "deploymentConfigInfo": info.clone() }))
1457 }
1458
1459 fn delete_deployment_config(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1460 let b = body(req);
1461 let name = req_deployment_config_name(&b)?;
1462 if predefined_config(&name).is_some() {
1463 return Err(e(
1464 "InvalidOperationException",
1465 "A predefined deployment configuration cannot be deleted.",
1466 ));
1467 }
1468 let (region, account) = self.region_account(req);
1469 let mut guard = self.state.write();
1470 let st = guard.get_or_create(&account);
1471 if st.deployment_configs.remove(&name).is_some() {
1472 st.deployment_config_order.retain(|n| n != &name);
1473 }
1474 st.tags
1476 .remove(&deployment_config_arn(®ion, &account, &name));
1477 ok(json!({}))
1478 }
1479
1480 fn list_deployment_configs(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1481 let b = body(req);
1482 let (_region, account) = self.region_account(req);
1483 let mut guard = self.state.write();
1484 let st = guard.get_or_create(&account);
1485 let mut names: Vec<Value> = PREDEFINED_CONFIG_NAMES.iter().map(|n| json!(n)).collect();
1486 names.extend(st.deployment_config_order.iter().map(|n| json!(n)));
1487 ok(paginate_body(names, "deploymentConfigsList", &b)?)
1488 }
1489}
1490
1491fn settle_deployment(info: &mut Value, step: &mut u8) {
1500 let status = info
1501 .get("status")
1502 .and_then(Value::as_str)
1503 .unwrap_or("Created");
1504 if matches!(status, "Succeeded" | "Failed" | "Stopped") {
1505 return;
1506 }
1507 let now = Utc::now();
1508 *step = step.saturating_add(1);
1509 if let Some(obj) = info.as_object_mut() {
1510 if *step == 1 {
1511 obj.insert("status".into(), json!("InProgress"));
1512 obj.entry("startTime").or_insert_with(|| ts(now));
1513 obj.insert(
1514 "deploymentOverview".into(),
1515 json!({ "Pending": 0, "InProgress": 1, "Succeeded": 0, "Failed": 0, "Skipped": 0, "Ready": 0 }),
1516 );
1517 } else {
1518 obj.insert("status".into(), json!("Succeeded"));
1519 obj.entry("startTime").or_insert_with(|| ts(now));
1520 obj.insert("completeTime".into(), ts(now));
1521 obj.insert(
1522 "deploymentOverview".into(),
1523 json!({ "Pending": 0, "InProgress": 0, "Succeeded": 1, "Failed": 0, "Skipped": 0, "Ready": 0 }),
1524 );
1525 }
1526 }
1527}
1528
1529impl CodeDeployService {
1530 fn create_deployment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1531 let b = body(req);
1532 let app = req_application_name(&b)?;
1533 let dg_name = req_deployment_group_name(&b, "deploymentGroupName")?;
1537 if let Some(v) = str_field(&b, "deploymentConfigName") {
1538 check_deployment_config_name(&v)?;
1539 }
1540 opt_enum(
1541 &b,
1542 "fileExistsBehavior",
1543 validate::FILE_EXISTS_BEHAVIOR,
1544 "InvalidFileExistsBehaviorException",
1545 "The specified fileExistsBehavior value is invalid.",
1546 )?;
1547 if let Some(rev) = b.get("revision").filter(|v| !v.is_null()) {
1548 validate_revision(rev)?;
1549 }
1550 let (_region, account) = self.region_account(req);
1551 let now = Utc::now();
1552 let mut guard = self.state.write();
1553 let st = guard.get_or_create(&account);
1554 if !st.applications.contains_key(&app) {
1555 return Err(e(
1556 "ApplicationDoesNotExistException",
1557 format!("No application found for name: {app}"),
1558 ));
1559 }
1560 if !st
1561 .deployment_groups
1562 .get(&app)
1563 .map(|g| g.contains_key(&dg_name))
1564 .unwrap_or(false)
1565 {
1566 return Err(e(
1567 "DeploymentGroupDoesNotExistException",
1568 format!("No deployment group found for name: {dg_name}"),
1569 ));
1570 }
1571 let config_name = str_field(&b, "deploymentConfigName").unwrap_or_else(|| {
1572 st.deployment_groups
1573 .get(&app)
1574 .and_then(|g| g.get(&dg_name))
1575 .and_then(|g| g.get("deploymentConfigName").and_then(Value::as_str))
1576 .unwrap_or("CodeDeployDefault.OneAtATime")
1577 .to_string()
1578 });
1579 if predefined_config(&config_name).is_none()
1580 && !st.deployment_configs.contains_key(&config_name)
1581 {
1582 return Err(e(
1583 "DeploymentConfigDoesNotExistException",
1584 format!("No deployment configuration found for name: {config_name}"),
1585 ));
1586 }
1587 let compute = Self::app_compute_platform(st, &app);
1588 let deployment_style = st
1591 .deployment_groups
1592 .get(&app)
1593 .and_then(|g| g.get(&dg_name))
1594 .and_then(|g| g.get("deploymentStyle").cloned())
1595 .unwrap_or_else(|| {
1596 json!({ "deploymentType": "IN_PLACE", "deploymentOption": "WITHOUT_TRAFFIC_CONTROL" })
1597 });
1598 let deployment_id = gen_deployment_id();
1599 let mut info = Map::new();
1600 info.insert("deploymentId".into(), json!(deployment_id));
1601 info.insert("applicationName".into(), json!(app));
1602 info.insert("deploymentGroupName".into(), json!(dg_name));
1603 info.insert("deploymentConfigName".into(), json!(config_name));
1604 info.insert("computePlatform".into(), json!(compute));
1605 info.insert("status".into(), json!("Created"));
1606 info.insert("creator".into(), json!("user"));
1607 info.insert("createTime".into(), ts(now));
1608 info.insert("deploymentStyle".into(), deployment_style);
1609 info.insert(
1610 "ignoreApplicationStopFailures".into(),
1611 json!(b
1612 .get("ignoreApplicationStopFailures")
1613 .and_then(Value::as_bool)
1614 .unwrap_or(false)),
1615 );
1616 info.insert(
1617 "updateOutdatedInstancesOnly".into(),
1618 json!(b
1619 .get("updateOutdatedInstancesOnly")
1620 .and_then(Value::as_bool)
1621 .unwrap_or(false)),
1622 );
1623 for (in_key, out_key) in [
1624 ("revision", "revision"),
1625 ("description", "description"),
1626 ("autoRollbackConfiguration", "autoRollbackConfiguration"),
1627 ("fileExistsBehavior", "fileExistsBehavior"),
1628 ("targetInstances", "targetInstances"),
1629 ("overrideAlarmConfiguration", "overrideAlarmConfiguration"),
1630 ] {
1631 if let Some(v) = b.get(in_key).filter(|v| !v.is_null()) {
1632 info.insert(out_key.into(), v.clone());
1633 }
1634 }
1635 let info = Value::Object(info);
1636 st.deployments.insert(deployment_id.clone(), info);
1637 st.deployment_order.push(deployment_id.clone());
1638 st.deployment_settle.insert(deployment_id.clone(), 0);
1639 ok(json!({ "deploymentId": deployment_id }))
1640 }
1641
1642 fn get_deployment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1643 let b = body(req);
1644 let id = req_deployment_id(&b)?;
1645 let (_region, account) = self.region_account(req);
1646 let mut guard = self.state.write();
1647 let st = guard.get_or_create(&account);
1648 let mut step = st.deployment_settle.get(&id).copied().unwrap_or(0);
1649 let Some(info) = st.deployments.get_mut(&id) else {
1650 return Err(e(
1651 "DeploymentDoesNotExistException",
1652 format!("The deployment {id} could not be found."),
1653 ));
1654 };
1655 settle_deployment(info, &mut step);
1656 let info = info.clone();
1657 st.deployment_settle.insert(id, step);
1658 ok(json!({ "deploymentInfo": info }))
1659 }
1660
1661 fn batch_get_deployments(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1662 let b = body(req);
1663 let ids = str_list(&b, "deploymentIds");
1664 let (_region, account) = self.region_account(req);
1665 let mut guard = self.state.write();
1666 let st = guard.get_or_create(&account);
1667 let mut infos = Vec::new();
1668 for id in ids {
1669 let mut step = st.deployment_settle.get(&id).copied().unwrap_or(0);
1670 if let Some(info) = st.deployments.get_mut(&id) {
1671 settle_deployment(info, &mut step);
1672 infos.push(info.clone());
1673 st.deployment_settle.insert(id, step);
1674 }
1675 }
1676 ok(json!({ "deploymentsInfo": infos }))
1677 }
1678
1679 fn list_deployments(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1680 let b = body(req);
1681 if let Some(v) = str_field(&b, "applicationName") {
1682 check_application_name(&v)?;
1683 }
1684 if let Some(v) = str_field(&b, "deploymentGroupName") {
1685 check_deployment_group_name(&v)?;
1686 }
1687 if str_field(&b, "deploymentGroupName").is_some()
1689 && str_field(&b, "applicationName").is_none()
1690 {
1691 return Err(e(
1692 "ApplicationNameRequiredException",
1693 "The minimum number of required application inputs was not satisfied.",
1694 ));
1695 }
1696 let status_filter: Vec<String> = str_list(&b, "includeOnlyStatuses");
1698 for s in &status_filter {
1699 if !validate::is_enum(validate::DEPLOYMENT_STATUS, s) {
1700 return Err(e(
1701 "InvalidDeploymentStatusException",
1702 format!("The deployment status is invalid: {s}"),
1703 ));
1704 }
1705 }
1706 let time_range = b.get("createTimeRange").filter(|v| !v.is_null());
1708 let range_start = time_range
1709 .and_then(|r| r.get("start"))
1710 .and_then(Value::as_f64);
1711 let range_end = time_range
1712 .and_then(|r| r.get("end"))
1713 .and_then(Value::as_f64);
1714 if let (Some(start), Some(end)) = (range_start, range_end) {
1715 if start > end {
1716 return Err(e(
1717 "InvalidTimeRangeException",
1718 "The specified time range is invalid: the start is after the end.",
1719 ));
1720 }
1721 }
1722 let (_region, account) = self.region_account(req);
1723 let mut guard = self.state.write();
1724 let st = guard.get_or_create(&account);
1725 let app_filter = str_field(&b, "applicationName");
1726 if let Some(app) = &app_filter {
1727 if !st.applications.contains_key(app) {
1728 return Err(e(
1729 "ApplicationDoesNotExistException",
1730 format!("No application found for name: {app}"),
1731 ));
1732 }
1733 }
1734 let dg_filter = str_field(&b, "deploymentGroupName");
1735 let ids: Vec<Value> = st
1736 .deployment_order
1737 .iter()
1738 .rev()
1739 .filter(|id| {
1740 let info = match st.deployments.get(*id) {
1741 Some(i) => i,
1742 None => return false,
1743 };
1744 if let Some(app) = &app_filter {
1745 if info.get("applicationName").and_then(Value::as_str) != Some(app.as_str()) {
1746 return false;
1747 }
1748 }
1749 if let Some(dg) = &dg_filter {
1750 if info.get("deploymentGroupName").and_then(Value::as_str) != Some(dg.as_str())
1751 {
1752 return false;
1753 }
1754 }
1755 if !status_filter.is_empty() {
1756 let status = info
1757 .get("status")
1758 .and_then(Value::as_str)
1759 .unwrap_or("Created");
1760 if !status_filter.iter().any(|s| s == status) {
1761 return false;
1762 }
1763 }
1764 if range_start.is_some() || range_end.is_some() {
1765 let created = info
1766 .get("createTime")
1767 .and_then(Value::as_f64)
1768 .unwrap_or(0.0);
1769 if let Some(start) = range_start {
1770 if created < start {
1771 return false;
1772 }
1773 }
1774 if let Some(end) = range_end {
1775 if created > end {
1776 return false;
1777 }
1778 }
1779 }
1780 true
1781 })
1782 .map(|id| json!(id))
1783 .collect();
1784 ok(paginate_body(ids, "deployments", &b)?)
1785 }
1786
1787 fn stop_deployment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1788 let b = body(req);
1789 let id = req_deployment_id(&b)?;
1790 let (_region, account) = self.region_account(req);
1791 let mut guard = self.state.write();
1792 let st = guard.get_or_create(&account);
1793 let Some(info) = st.deployments.get_mut(&id) else {
1794 return Err(e(
1795 "DeploymentDoesNotExistException",
1796 format!("The deployment {id} could not be found."),
1797 ));
1798 };
1799 let status = info
1800 .get("status")
1801 .and_then(Value::as_str)
1802 .unwrap_or("Created");
1803 if matches!(status, "Succeeded" | "Failed" | "Stopped") {
1804 return Err(e(
1805 "DeploymentAlreadyCompletedException",
1806 "The specified deployment has already completed.",
1807 ));
1808 }
1809 if let Some(obj) = info.as_object_mut() {
1810 obj.insert("status".into(), json!("Stopped"));
1811 obj.insert("completeTime".into(), ts(Utc::now()));
1812 }
1813 ok(json!({ "status": "Succeeded", "statusMessage": "Deployment was stopped." }))
1814 }
1815
1816 fn continue_deployment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1817 let b = body(req);
1818 opt_enum(
1819 &b,
1820 "deploymentWaitType",
1821 validate::DEPLOYMENT_WAIT_TYPE,
1822 "InvalidDeploymentWaitTypeException",
1823 "The wait type is invalid.",
1824 )?;
1825 if let Some(id) = str_field(&b, "deploymentId") {
1826 let (_region, account) = self.region_account(req);
1827 let mut guard = self.state.write();
1828 let st = guard.get_or_create(&account);
1829 if !st.deployments.contains_key(&id) {
1830 return Err(e(
1831 "DeploymentDoesNotExistException",
1832 format!("The deployment {id} could not be found."),
1833 ));
1834 }
1835 }
1836 ok(json!({}))
1837 }
1838
1839 fn skip_wait_time(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1840 let b = body(req);
1841 if let Some(id) = str_field(&b, "deploymentId") {
1842 let (_region, account) = self.region_account(req);
1843 let mut guard = self.state.write();
1844 let st = guard.get_or_create(&account);
1845 if !st.deployments.contains_key(&id) {
1846 return Err(e(
1847 "DeploymentDoesNotExistException",
1848 format!("The deployment {id} could not be found."),
1849 ));
1850 }
1851 }
1852 ok(json!({}))
1853 }
1854}
1855
1856impl CodeDeployService {
1861 fn require_deployment(st: &CodeDeployState, id: &str) -> Result<(), AwsServiceError> {
1862 if st.deployments.contains_key(id) {
1863 Ok(())
1864 } else {
1865 Err(e(
1866 "DeploymentDoesNotExistException",
1867 format!("The deployment {id} could not be found."),
1868 ))
1869 }
1870 }
1871
1872 fn get_deployment_target(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1873 let b = body(req);
1874 let id = req_deployment_id(&b)?;
1875 let Some(target_id) = str_field(&b, "targetId").filter(|s| !s.is_empty()) else {
1876 return Err(e(
1877 "DeploymentTargetIdRequiredException",
1878 "A deployment target ID must be specified.",
1879 ));
1880 };
1881 let (_region, account) = self.region_account(req);
1882 let mut guard = self.state.write();
1883 let st = guard.get_or_create(&account);
1884 Self::require_deployment(st, &id)?;
1885 Err(e(
1888 "DeploymentTargetDoesNotExistException",
1889 format!("The deployment target {target_id} could not be found."),
1890 ))
1891 }
1892
1893 fn list_deployment_targets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1894 let b = body(req);
1895 let id = req_deployment_id(&b)?;
1896 let (_region, account) = self.region_account(req);
1897 let mut guard = self.state.write();
1898 let st = guard.get_or_create(&account);
1899 Self::require_deployment(st, &id)?;
1900 ok(paginate_body(vec![], "targetIds", &b)?)
1901 }
1902
1903 fn batch_get_deployment_targets(
1904 &self,
1905 req: &AwsRequest,
1906 ) -> Result<AwsResponse, AwsServiceError> {
1907 let b = body(req);
1908 let id = req_deployment_id(&b)?;
1909 let (_region, account) = self.region_account(req);
1910 let mut guard = self.state.write();
1911 let st = guard.get_or_create(&account);
1912 Self::require_deployment(st, &id)?;
1913 ok(json!({ "deploymentTargets": [] }))
1914 }
1915
1916 fn get_deployment_instance(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1917 let b = body(req);
1918 let id = req_deployment_id(&b)?;
1919 let Some(instance_id) = str_field(&b, "instanceId").filter(|s| !s.is_empty()) else {
1920 return Err(e(
1921 "InstanceIdRequiredException",
1922 "The instance ID was not specified.",
1923 ));
1924 };
1925 let (_region, account) = self.region_account(req);
1926 let mut guard = self.state.write();
1927 let st = guard.get_or_create(&account);
1928 Self::require_deployment(st, &id)?;
1929 Err(e(
1930 "InstanceDoesNotExistException",
1931 format!("The instance {instance_id} could not be found."),
1932 ))
1933 }
1934
1935 fn list_deployment_instances(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1936 let b = body(req);
1937 let id = req_deployment_id(&b)?;
1938 let (_region, account) = self.region_account(req);
1939 let mut guard = self.state.write();
1940 let st = guard.get_or_create(&account);
1941 Self::require_deployment(st, &id)?;
1942 ok(paginate_body(vec![], "instancesList", &b)?)
1943 }
1944
1945 fn batch_get_deployment_instances(
1946 &self,
1947 req: &AwsRequest,
1948 ) -> Result<AwsResponse, AwsServiceError> {
1949 let b = body(req);
1950 let id = req_deployment_id(&b)?;
1951 let (_region, account) = self.region_account(req);
1952 let mut guard = self.state.write();
1953 let st = guard.get_or_create(&account);
1954 Self::require_deployment(st, &id)?;
1955 ok(json!({ "instancesSummary": [] }))
1956 }
1957}
1958
1959impl CodeDeployService {
1964 fn register_on_premises_instance(
1965 &self,
1966 req: &AwsRequest,
1967 ) -> Result<AwsResponse, AwsServiceError> {
1968 let b = body(req);
1969 let name = req_instance_name(&b)?;
1970 let iam_user = str_field(&b, "iamUserArn");
1971 let iam_session = str_field(&b, "iamSessionArn");
1972 match (&iam_user, &iam_session) {
1973 (None, None) => return Err(e("IamArnRequiredException", "No IAM ARN was specified.")),
1974 (Some(_), Some(_)) => {
1975 return Err(e(
1976 "MultipleIamArnsProvidedException",
1977 "Both an IAM user ARN and an IAM session ARN were specified.",
1978 ))
1979 }
1980 _ => {}
1981 }
1982 let (region, account) = self.region_account(req);
1983 let now = Utc::now();
1984 let mut guard = self.state.write();
1985 let st = guard.get_or_create(&account);
1986 let already_registered = st
1990 .on_premises_instances
1991 .get(&name)
1992 .map(|i| i.get("deregisterTime").is_none())
1993 .unwrap_or(false);
1994 if already_registered {
1995 return Err(e(
1996 "InstanceNameAlreadyRegisteredException",
1997 format!("The specified on-premises instance name is already registered: {name}"),
1998 ));
1999 }
2000 let instance_arn = format!("arn:aws:codedeploy:{region}:{account}:instance/{name}");
2001 let mut info = Map::new();
2002 info.insert("instanceName".into(), json!(name));
2003 info.insert("instanceArn".into(), json!(instance_arn));
2004 info.insert("registerTime".into(), ts(now));
2005 info.insert("tags".into(), json!([]));
2006 if let Some(u) = iam_user {
2007 info.insert("iamUserArn".into(), json!(u));
2008 }
2009 if let Some(s) = iam_session {
2010 info.insert("iamSessionArn".into(), json!(s));
2011 }
2012 let is_new = !st.on_premises_instances.contains_key(&name);
2013 st.on_premises_instances
2014 .insert(name.clone(), Value::Object(info));
2015 if is_new {
2016 st.on_premises_order.push(name);
2017 }
2018 ok(json!({}))
2019 }
2020
2021 fn deregister_on_premises_instance(
2022 &self,
2023 req: &AwsRequest,
2024 ) -> Result<AwsResponse, AwsServiceError> {
2025 let b = body(req);
2026 let name = req_instance_name(&b)?;
2027 let (_region, account) = self.region_account(req);
2028 let now = Utc::now();
2029 let mut guard = self.state.write();
2030 let st = guard.get_or_create(&account);
2031 if let Some(info) = st.on_premises_instances.get_mut(&name) {
2036 if let Some(obj) = info.as_object_mut() {
2037 obj.insert("deregisterTime".into(), ts(now));
2038 }
2039 }
2040 ok(json!({}))
2041 }
2042
2043 fn get_on_premises_instance(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2044 let b = body(req);
2045 let name = req_instance_name(&b)?;
2046 let (_region, account) = self.region_account(req);
2047 let mut guard = self.state.write();
2048 let st = guard.get_or_create(&account);
2049 let Some(info) = st.on_premises_instances.get(&name) else {
2050 return Err(e(
2051 "InstanceNotRegisteredException",
2052 format!("The on-premises instance is not registered: {name}"),
2053 ));
2054 };
2055 ok(json!({ "instanceInfo": info.clone() }))
2056 }
2057
2058 fn list_on_premises_instances(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2059 let b = body(req);
2060 opt_enum(
2061 &b,
2062 "registrationStatus",
2063 validate::REGISTRATION_STATUS,
2064 "InvalidRegistrationStatusException",
2065 "The registration status was specified in an invalid format.",
2066 )?;
2067 let (_region, account) = self.region_account(req);
2068 let mut guard = self.state.write();
2069 let st = guard.get_or_create(&account);
2070 let status_filter = str_field(&b, "registrationStatus");
2073 let names: Vec<Value> = st
2074 .on_premises_order
2075 .iter()
2076 .filter(|n| {
2077 let deregistered = st
2078 .on_premises_instances
2079 .get(*n)
2080 .map(|i| i.get("deregisterTime").is_some())
2081 .unwrap_or(false);
2082 match status_filter.as_deref() {
2083 Some("Registered") => !deregistered,
2084 Some("Deregistered") => deregistered,
2085 _ => true,
2086 }
2087 })
2088 .map(|n| json!(n))
2089 .collect();
2090 ok(paginate_body(names, "instanceNames", &b)?)
2091 }
2092
2093 fn batch_get_on_premises_instances(
2094 &self,
2095 req: &AwsRequest,
2096 ) -> Result<AwsResponse, AwsServiceError> {
2097 let b = body(req);
2098 let names = str_list(&b, "instanceNames");
2099 let (_region, account) = self.region_account(req);
2100 let mut guard = self.state.write();
2101 let st = guard.get_or_create(&account);
2102 let infos: Vec<Value> = names
2103 .iter()
2104 .filter_map(|n| st.on_premises_instances.get(n).cloned())
2105 .collect();
2106 ok(json!({ "instanceInfos": infos }))
2107 }
2108
2109 fn add_tags_to_on_premises(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2110 let b = body(req);
2111 let tags = b
2112 .get("tags")
2113 .and_then(Value::as_array)
2114 .cloned()
2115 .unwrap_or_default();
2116 if tags.is_empty() {
2117 return Err(e("TagRequiredException", "A tag was not specified."));
2118 }
2119 let names = str_list(&b, "instanceNames");
2120 if names.is_empty() {
2121 return Err(e(
2122 "InstanceNameRequiredException",
2123 "An on-premises instance name was not specified.",
2124 ));
2125 }
2126 let (_region, account) = self.region_account(req);
2127 let mut guard = self.state.write();
2128 let st = guard.get_or_create(&account);
2129 Self::require_all_registered(st, &names)?;
2133 for name in &names {
2134 if let Some(existing) = st
2135 .on_premises_instances
2136 .get_mut(name)
2137 .and_then(|i| i.get_mut("tags"))
2138 .and_then(Value::as_array_mut)
2139 {
2140 for t in &tags {
2141 if !existing.contains(t) {
2142 existing.push(t.clone());
2143 }
2144 }
2145 }
2146 }
2147 ok(json!({}))
2148 }
2149
2150 fn remove_tags_from_on_premises(
2151 &self,
2152 req: &AwsRequest,
2153 ) -> Result<AwsResponse, AwsServiceError> {
2154 let b = body(req);
2155 let tags = b
2156 .get("tags")
2157 .and_then(Value::as_array)
2158 .cloned()
2159 .unwrap_or_default();
2160 if tags.is_empty() {
2161 return Err(e("TagRequiredException", "A tag was not specified."));
2162 }
2163 let names = str_list(&b, "instanceNames");
2164 if names.is_empty() {
2165 return Err(e(
2166 "InstanceNameRequiredException",
2167 "An on-premises instance name was not specified.",
2168 ));
2169 }
2170 let (_region, account) = self.region_account(req);
2171 let mut guard = self.state.write();
2172 let st = guard.get_or_create(&account);
2173 Self::require_all_registered(st, &names)?;
2176 for name in &names {
2177 if let Some(existing) = st
2178 .on_premises_instances
2179 .get_mut(name)
2180 .and_then(|i| i.get_mut("tags"))
2181 .and_then(Value::as_array_mut)
2182 {
2183 existing.retain(|t| !tags.contains(t));
2184 }
2185 }
2186 ok(json!({}))
2187 }
2188
2189 fn require_all_registered(
2193 st: &CodeDeployState,
2194 names: &[String],
2195 ) -> Result<(), AwsServiceError> {
2196 for name in names {
2197 let registered = st
2198 .on_premises_instances
2199 .get(name)
2200 .map(|i| i.get("deregisterTime").is_none())
2201 .unwrap_or(false);
2202 if !registered {
2203 return Err(e(
2204 "InstanceNotRegisteredException",
2205 format!("The on-premises instance is not registered: {name}"),
2206 ));
2207 }
2208 }
2209 Ok(())
2210 }
2211}
2212
2213impl CodeDeployService {
2218 fn list_github_account_token_names(
2219 &self,
2220 req: &AwsRequest,
2221 ) -> Result<AwsResponse, AwsServiceError> {
2222 let b = body(req);
2223 ok(paginate_body(vec![], "tokenNameList", &b)?)
2225 }
2226
2227 fn delete_github_account_token(
2228 &self,
2229 req: &AwsRequest,
2230 ) -> Result<AwsResponse, AwsServiceError> {
2231 let b = body(req);
2232 let Some(token_name) = str_field(&b, "tokenName") else {
2233 return Err(e(
2234 "GitHubAccountTokenNameRequiredException",
2235 "The GitHub account token name was not specified.",
2236 ));
2237 };
2238 Err(e(
2240 "GitHubAccountTokenDoesNotExistException",
2241 format!("No GitHub account connection exists for token name: {token_name}"),
2242 ))
2243 }
2244
2245 fn put_lifecycle_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2246 let b = body(req);
2247 opt_enum(
2248 &b,
2249 "status",
2250 validate::LIFECYCLE_EVENT_STATUS,
2251 "InvalidLifecycleEventHookExecutionStatusException",
2252 "The lifecycle event status was specified in an invalid format.",
2253 )?;
2254 if let Some(id) = str_field(&b, "deploymentId") {
2255 let (_region, account) = self.region_account(req);
2256 let mut guard = self.state.write();
2257 let st = guard.get_or_create(&account);
2258 if !st.deployments.contains_key(&id) {
2259 return Err(e(
2260 "DeploymentDoesNotExistException",
2261 format!("The deployment {id} could not be found."),
2262 ));
2263 }
2264 }
2265 let exec_id = str_field(&b, "lifecycleEventHookExecutionId")
2266 .unwrap_or_else(|| uuid::Uuid::new_v4().simple().to_string());
2267 ok(json!({ "lifecycleEventHookExecutionId": exec_id }))
2268 }
2269
2270 fn delete_resources_by_external_id(
2271 &self,
2272 req: &AwsRequest,
2273 ) -> Result<AwsResponse, AwsServiceError> {
2274 let b = body(req);
2275 let Some(external_id) = str_field(&b, "externalId") else {
2276 return ok(json!({}));
2277 };
2278 let (_region, account) = self.region_account(req);
2279 let mut guard = self.state.write();
2280 let st = guard.get_or_create(&account);
2281 let to_remove: Vec<String> = st
2286 .deployments
2287 .iter()
2288 .filter(|(_, info)| {
2289 info.get("externalId").and_then(Value::as_str) == Some(external_id.as_str())
2290 })
2291 .map(|(id, _)| id.clone())
2292 .collect();
2293 for id in to_remove {
2294 st.deployments.remove(&id);
2295 st.deployment_settle.remove(&id);
2296 st.deployment_order.retain(|x| x != &id);
2297 }
2298 ok(json!({}))
2299 }
2300}
2301
2302impl CodeDeployService {
2307 fn require_resource_exists(st: &CodeDeployState, arn: &str) -> Result<(), AwsServiceError> {
2312 let parts: Vec<&str> = arn.splitn(7, ':').collect();
2313 let (kind, rest) = (parts[5], parts.get(6).copied().unwrap_or(""));
2314 let group_exists = |rest: &str| {
2315 let (app, group) = rest.split_once('/').unwrap_or((rest, ""));
2316 st.deployment_groups
2317 .get(app)
2318 .map(|g| g.contains_key(group))
2319 .unwrap_or(false)
2320 };
2321 match kind {
2322 "application" if !st.applications.contains_key(rest) => Err(e(
2323 "ApplicationDoesNotExistException",
2324 format!("No application found for name: {rest}"),
2325 )),
2326 "deploymentgroup" if !group_exists(rest) => Err(e(
2327 "DeploymentGroupDoesNotExistException",
2328 format!("No deployment group found for name: {rest}"),
2329 )),
2330 "deploymentconfig"
2331 if predefined_config(rest).is_none()
2332 && !st.deployment_configs.contains_key(rest) =>
2333 {
2334 Err(e(
2335 "DeploymentConfigDoesNotExistException",
2336 format!("No deployment configuration found for name: {rest}"),
2337 ))
2338 }
2339 _ => Ok(()),
2340 }
2341 }
2342
2343 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2344 let b = body(req);
2345 let arn = req_resource_arn(&b)?;
2346 let tags = b
2347 .get("Tags")
2348 .and_then(Value::as_array)
2349 .cloned()
2350 .unwrap_or_default();
2351 if tags.is_empty() {
2352 return Err(e("TagRequiredException", "A tag was not specified."));
2353 }
2354 let (_region, account) = self.region_account(req);
2355 let mut guard = self.state.write();
2356 let st = guard.get_or_create(&account);
2357 Self::require_resource_exists(st, &arn)?;
2358 let stored = st.tags.entry(arn).or_default();
2359 for t in tags {
2360 let key = t.get("Key").and_then(Value::as_str).map(str::to_string);
2361 if let Some(k) = key {
2362 stored.retain(|x| x.get("Key").and_then(Value::as_str) != Some(k.as_str()));
2363 }
2364 stored.push(t);
2365 }
2366 ok(json!({}))
2367 }
2368
2369 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2370 let b = body(req);
2371 let arn = req_resource_arn(&b)?;
2372 let keys = str_list(&b, "TagKeys");
2373 let (_region, account) = self.region_account(req);
2374 let mut guard = self.state.write();
2375 let st = guard.get_or_create(&account);
2376 Self::require_resource_exists(st, &arn)?;
2377 if let Some(stored) = st.tags.get_mut(&arn) {
2378 stored.retain(|t| {
2379 t.get("Key")
2380 .and_then(Value::as_str)
2381 .map(|k| !keys.iter().any(|rk| rk == k))
2382 .unwrap_or(true)
2383 });
2384 }
2385 ok(json!({}))
2386 }
2387
2388 fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2389 let b = body(req);
2390 let arn = req_resource_arn(&b)?;
2391 let (_region, account) = self.region_account(req);
2392 let mut guard = self.state.write();
2393 let st = guard.get_or_create(&account);
2394 let tags = st.tags.get(&arn).cloned().unwrap_or_default();
2395 let out = paginate_body(tags, "Tags", &b)?;
2396 ok(out)
2397 }
2398}
2399
2400#[cfg(test)]
2401mod tests {
2402 use super::*;
2403 use fakecloud_core::multi_account::MultiAccountState;
2404 use parking_lot::RwLock;
2405
2406 fn svc() -> CodeDeployService {
2407 let state = Arc::new(RwLock::new(MultiAccountState::new(
2408 "000000000000",
2409 "us-east-1",
2410 "",
2411 )));
2412 CodeDeployService::new(state)
2413 }
2414
2415 fn req(action: &str, body: Value) -> AwsRequest {
2416 AwsRequest {
2417 service: "codedeploy".to_string(),
2418 action: action.to_string(),
2419 region: "us-east-1".to_string(),
2420 account_id: "000000000000".to_string(),
2421 request_id: "rid".to_string(),
2422 headers: http::HeaderMap::new(),
2423 query_params: std::collections::HashMap::new(),
2424 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
2425 body_stream: parking_lot::Mutex::new(None),
2426 path_segments: vec![],
2427 raw_path: "/".to_string(),
2428 raw_query: String::new(),
2429 method: http::Method::POST,
2430 is_query_protocol: false,
2431 access_key_id: None,
2432 principal: None,
2433 }
2434 }
2435
2436 fn body_of(resp: AwsResponse) -> Value {
2437 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
2438 }
2439
2440 fn err_code(r: Result<AwsResponse, AwsServiceError>) -> String {
2441 match r {
2442 Ok(_) => panic!("expected an error"),
2443 Err(e) => e.code().to_string(),
2444 }
2445 }
2446
2447 #[test]
2448 fn create_get_application_round_trip() {
2449 let s = svc();
2450 let created = body_of(
2451 s.create_application(&req(
2452 "CreateApplication",
2453 json!({ "applicationName": "app1", "computePlatform": "Lambda" }),
2454 ))
2455 .unwrap(),
2456 );
2457 assert!(created["applicationId"].is_string());
2458 let got = body_of(
2459 s.get_application(&req("GetApplication", json!({ "applicationName": "app1" })))
2460 .unwrap(),
2461 );
2462 assert_eq!(got["application"]["applicationName"], "app1");
2463 assert_eq!(got["application"]["computePlatform"], "Lambda");
2464 }
2465
2466 #[test]
2467 fn create_application_rejects_duplicate_and_bad_platform() {
2468 let s = svc();
2469 s.create_application(&req(
2470 "CreateApplication",
2471 json!({ "applicationName": "dup" }),
2472 ))
2473 .unwrap();
2474 assert_eq!(
2475 err_code(s.create_application(&req(
2476 "CreateApplication",
2477 json!({ "applicationName": "dup" })
2478 ))),
2479 "ApplicationAlreadyExistsException"
2480 );
2481 assert_eq!(
2482 err_code(s.create_application(&req(
2483 "CreateApplication",
2484 json!({ "applicationName": "x", "computePlatform": "NOPE" })
2485 ))),
2486 "InvalidComputePlatformException"
2487 );
2488 }
2489
2490 #[test]
2491 fn application_name_required_and_invalid() {
2492 let s = svc();
2493 assert_eq!(
2494 err_code(s.get_application(&req("GetApplication", json!({})))),
2495 "ApplicationNameRequiredException"
2496 );
2497 let long = "a".repeat(101);
2498 assert_eq!(
2499 err_code(s.get_application(&req("GetApplication", json!({ "applicationName": long })))),
2500 "InvalidApplicationNameException"
2501 );
2502 assert_eq!(
2503 err_code(s.get_application(&req(
2504 "GetApplication",
2505 json!({ "applicationName": "ghost" })
2506 ))),
2507 "ApplicationDoesNotExistException"
2508 );
2509 }
2510
2511 #[test]
2512 fn predefined_deployment_config_always_present() {
2513 let s = svc();
2514 let got = body_of(
2515 s.get_deployment_config(&req(
2516 "GetDeploymentConfig",
2517 json!({ "deploymentConfigName": "CodeDeployDefault.OneAtATime" }),
2518 ))
2519 .unwrap(),
2520 );
2521 assert_eq!(got["deploymentConfigInfo"]["computePlatform"], "Server");
2522 assert_eq!(
2523 got["deploymentConfigInfo"]["minimumHealthyHosts"]["type"],
2524 "HOST_COUNT"
2525 );
2526 let canary = body_of(s.get_deployment_config(&req("GetDeploymentConfig", json!({ "deploymentConfigName": "CodeDeployDefault.LambdaCanary10Percent5Minutes" }))).unwrap());
2527 assert_eq!(
2528 canary["deploymentConfigInfo"]["trafficRoutingConfig"]["type"],
2529 "TimeBasedCanary"
2530 );
2531 }
2532
2533 #[test]
2534 fn deployment_settles_through_lifecycle() {
2535 let s = svc();
2536 s.create_application(&req(
2537 "CreateApplication",
2538 json!({ "applicationName": "da" }),
2539 ))
2540 .unwrap();
2541 s.create_deployment_group(&req(
2542 "CreateDeploymentGroup",
2543 json!({ "applicationName": "da", "deploymentGroupName": "dg", "serviceRoleArn": "arn:aws:iam::000000000000:role/cd" }),
2544 ))
2545 .unwrap();
2546 let id = body_of(
2547 s.create_deployment(&req(
2548 "CreateDeployment",
2549 json!({ "applicationName": "da", "deploymentGroupName": "dg" }),
2550 ))
2551 .unwrap(),
2552 )["deploymentId"]
2553 .as_str()
2554 .unwrap()
2555 .to_string();
2556 assert!(id.starts_with("d-"));
2557 let first = body_of(
2558 s.get_deployment(&req("GetDeployment", json!({ "deploymentId": id.clone() })))
2559 .unwrap(),
2560 );
2561 assert_eq!(first["deploymentInfo"]["status"], "InProgress");
2562 let second = body_of(
2563 s.get_deployment(&req("GetDeployment", json!({ "deploymentId": id })))
2564 .unwrap(),
2565 );
2566 assert_eq!(second["deploymentInfo"]["status"], "Succeeded");
2567 }
2568
2569 #[test]
2570 fn create_deployment_group_requires_app_and_role() {
2571 let s = svc();
2572 assert_eq!(
2573 err_code(s.create_deployment_group(&req(
2574 "CreateDeploymentGroup",
2575 json!({ "applicationName": "ga", "deploymentGroupName": "g1" })
2576 ))),
2577 "RoleRequiredException"
2578 );
2579 assert_eq!(
2580 err_code(s.create_deployment_group(&req("CreateDeploymentGroup", json!({ "applicationName": "ga", "deploymentGroupName": "g1", "serviceRoleArn": "arn:aws:iam::000000000000:role/cd" })))),
2581 "ApplicationDoesNotExistException"
2582 );
2583 s.create_application(&req(
2584 "CreateApplication",
2585 json!({ "applicationName": "ga" }),
2586 ))
2587 .unwrap();
2588 let created = body_of(
2589 s.create_deployment_group(&req("CreateDeploymentGroup", json!({ "applicationName": "ga", "deploymentGroupName": "g1", "serviceRoleArn": "arn:aws:iam::000000000000:role/cd" })))
2590 .unwrap(),
2591 );
2592 assert!(created["deploymentGroupId"].is_string());
2593 let got = body_of(
2594 s.get_deployment_group(&req(
2595 "GetDeploymentGroup",
2596 json!({ "applicationName": "ga", "deploymentGroupName": "g1" }),
2597 ))
2598 .unwrap(),
2599 );
2600 assert_eq!(
2601 got["deploymentGroupInfo"]["deploymentConfigName"],
2602 "CodeDeployDefault.OneAtATime"
2603 );
2604 }
2605
2606 #[test]
2607 fn on_premises_register_requires_iam_arn() {
2608 let s = svc();
2609 assert_eq!(
2610 err_code(s.register_on_premises_instance(&req(
2611 "RegisterOnPremisesInstance",
2612 json!({ "instanceName": "i1" })
2613 ))),
2614 "IamArnRequiredException"
2615 );
2616 s.register_on_premises_instance(&req(
2617 "RegisterOnPremisesInstance",
2618 json!({ "instanceName": "i1", "iamUserArn": "arn:aws:iam::000000000000:user/u" }),
2619 ))
2620 .unwrap();
2621 let got = body_of(
2622 s.get_on_premises_instance(&req(
2623 "GetOnPremisesInstance",
2624 json!({ "instanceName": "i1" }),
2625 ))
2626 .unwrap(),
2627 );
2628 assert_eq!(got["instanceInfo"]["instanceName"], "i1");
2629 }
2630
2631 #[test]
2632 fn tag_resource_rejects_non_arn() {
2633 let s = svc();
2634 assert_eq!(
2635 err_code(s.list_tags_for_resource(&req(
2636 "ListTagsForResource",
2637 json!({ "ResourceArn": "not-an-arn" })
2638 ))),
2639 "InvalidArnException"
2640 );
2641 s.create_application(&req(
2642 "CreateApplication",
2643 json!({ "applicationName": "app1" }),
2644 ))
2645 .unwrap();
2646 let arn = "arn:aws:codedeploy:us-east-1:000000000000:application:app1";
2647 s.tag_resource(&req(
2648 "TagResource",
2649 json!({ "ResourceArn": arn, "Tags": [{ "Key": "env", "Value": "prod" }] }),
2650 ))
2651 .unwrap();
2652 let got = body_of(
2653 s.list_tags_for_resource(&req("ListTagsForResource", json!({ "ResourceArn": arn })))
2654 .unwrap(),
2655 );
2656 assert_eq!(got["Tags"][0]["Key"], "env");
2657 }
2658
2659 #[test]
2663 fn tag_resource_rejects_nonexistent_resource() {
2664 let s = svc();
2665 let ghost = "arn:aws:codedeploy:us-east-1:000000000000:application:ghost";
2666 assert_eq!(
2667 err_code(s.tag_resource(&req(
2668 "TagResource",
2669 json!({ "ResourceArn": ghost, "Tags": [{ "Key": "k", "Value": "v" }] })
2670 ))),
2671 "ApplicationDoesNotExistException"
2672 );
2673 let ghost_cfg = "arn:aws:codedeploy:us-east-1:000000000000:deploymentconfig:ghostcfg";
2674 assert_eq!(
2675 err_code(s.untag_resource(&req(
2676 "UntagResource",
2677 json!({ "ResourceArn": ghost_cfg, "TagKeys": ["k"] })
2678 ))),
2679 "DeploymentConfigDoesNotExistException"
2680 );
2681 let predef =
2683 "arn:aws:codedeploy:us-east-1:000000000000:deploymentconfig:CodeDeployDefault.OneAtATime";
2684 s.tag_resource(&req(
2685 "TagResource",
2686 json!({ "ResourceArn": predef, "Tags": [{ "Key": "k", "Value": "v" }] }),
2687 ))
2688 .unwrap();
2689 }
2690
2691 #[test]
2694 fn rename_application_rewrites_group_application_name() {
2695 let s = svc();
2696 s.create_application(&req("CreateApplication", json!({ "applicationName": "a" })))
2697 .unwrap();
2698 s.create_deployment_group(&req(
2699 "CreateDeploymentGroup",
2700 json!({
2701 "applicationName": "a",
2702 "deploymentGroupName": "g",
2703 "serviceRoleArn": "arn:aws:iam::000000000000:role/cd"
2704 }),
2705 ))
2706 .unwrap();
2707 s.update_application(&req(
2708 "UpdateApplication",
2709 json!({ "applicationName": "a", "newApplicationName": "b" }),
2710 ))
2711 .unwrap();
2712 let got = body_of(
2713 s.get_deployment_group(&req(
2714 "GetDeploymentGroup",
2715 json!({ "applicationName": "b", "deploymentGroupName": "g" }),
2716 ))
2717 .unwrap(),
2718 );
2719 assert_eq!(got["deploymentGroupInfo"]["applicationName"], "b");
2720 }
2721
2722 #[test]
2726 fn rename_application_rewrites_deployment_application_name() {
2727 let s = svc();
2728 s.create_application(&req("CreateApplication", json!({ "applicationName": "a" })))
2729 .unwrap();
2730 s.create_deployment_group(&req(
2731 "CreateDeploymentGroup",
2732 json!({ "applicationName": "a", "deploymentGroupName": "dg", "serviceRoleArn": "arn:aws:iam::000000000000:role/cd" }),
2733 ))
2734 .unwrap();
2735 let id = body_of(
2736 s.create_deployment(&req(
2737 "CreateDeployment",
2738 json!({ "applicationName": "a", "deploymentGroupName": "dg" }),
2739 ))
2740 .unwrap(),
2741 )["deploymentId"]
2742 .as_str()
2743 .unwrap()
2744 .to_string();
2745 s.update_application(&req(
2746 "UpdateApplication",
2747 json!({ "applicationName": "a", "newApplicationName": "b" }),
2748 ))
2749 .unwrap();
2750 let listed = body_of(
2752 s.list_deployments(&req("ListDeployments", json!({ "applicationName": "b" })))
2753 .unwrap(),
2754 );
2755 let ids: Vec<&str> = listed["deployments"]
2756 .as_array()
2757 .unwrap()
2758 .iter()
2759 .map(|v| v.as_str().unwrap())
2760 .collect();
2761 assert_eq!(ids, vec![id.as_str()]);
2762 let got = body_of(
2764 s.get_deployment(&req("GetDeployment", json!({ "deploymentId": id })))
2765 .unwrap(),
2766 );
2767 assert_eq!(got["deploymentInfo"]["applicationName"], "b");
2768 }
2769
2770 #[test]
2773 fn list_application_revisions_validates_enums() {
2774 let s = svc();
2775 s.create_application(&req(
2776 "CreateApplication",
2777 json!({ "applicationName": "ra" }),
2778 ))
2779 .unwrap();
2780 let cases = [
2781 ("deployed", "bogus", "InvalidDeployedStateFilterException"),
2782 ("sortBy", "bogus", "InvalidSortByException"),
2783 ("sortOrder", "bogus", "InvalidSortOrderException"),
2784 ];
2785 for (field, value, code) in cases {
2786 assert_eq!(
2787 err_code(s.list_application_revisions(&req(
2788 "ListApplicationRevisions",
2789 json!({ "applicationName": "ra", field: value }),
2790 ))),
2791 code,
2792 "field {field}",
2793 );
2794 }
2795 }
2796
2797 #[test]
2800 fn batch_get_applications_raises_not_found() {
2801 let s = svc();
2802 s.create_application(&req(
2803 "CreateApplication",
2804 json!({ "applicationName": "real" }),
2805 ))
2806 .unwrap();
2807 assert_eq!(
2808 err_code(s.batch_get_applications(&req(
2809 "BatchGetApplications",
2810 json!({ "applicationNames": ["real", "ghost"] })
2811 ))),
2812 "ApplicationDoesNotExistException"
2813 );
2814 let got = body_of(
2815 s.batch_get_applications(&req(
2816 "BatchGetApplications",
2817 json!({ "applicationNames": ["real"] }),
2818 ))
2819 .unwrap(),
2820 );
2821 assert_eq!(got["applicationsInfo"][0]["applicationName"], "real");
2822 }
2823
2824 #[test]
2827 fn list_deployments_filters_by_status_and_time() {
2828 let s = svc();
2829 s.create_application(&req(
2830 "CreateApplication",
2831 json!({ "applicationName": "app" }),
2832 ))
2833 .unwrap();
2834 s.create_deployment_group(&req(
2835 "CreateDeploymentGroup",
2836 json!({ "applicationName": "app", "deploymentGroupName": "dg", "serviceRoleArn": "arn:aws:iam::000000000000:role/cd" }),
2837 ))
2838 .unwrap();
2839 let mk = || {
2840 body_of(
2841 s.create_deployment(&req(
2842 "CreateDeployment",
2843 json!({ "applicationName": "app", "deploymentGroupName": "dg" }),
2844 ))
2845 .unwrap(),
2846 )["deploymentId"]
2847 .as_str()
2848 .unwrap()
2849 .to_string()
2850 };
2851 let created = mk(); let done = mk();
2853 s.get_deployment(&req(
2855 "GetDeployment",
2856 json!({ "deploymentId": done.clone() }),
2857 ))
2858 .unwrap();
2859 s.get_deployment(&req(
2860 "GetDeployment",
2861 json!({ "deploymentId": done.clone() }),
2862 ))
2863 .unwrap();
2864 let succeeded = body_of(
2865 s.list_deployments(&req(
2866 "ListDeployments",
2867 json!({ "includeOnlyStatuses": ["Succeeded"] }),
2868 ))
2869 .unwrap(),
2870 );
2871 let ids: Vec<&str> = succeeded["deployments"]
2872 .as_array()
2873 .unwrap()
2874 .iter()
2875 .map(|v| v.as_str().unwrap())
2876 .collect();
2877 assert_eq!(ids, vec![done.as_str()]);
2878 assert!(!ids.contains(&created.as_str()));
2879 assert_eq!(
2881 err_code(s.list_deployments(&req(
2882 "ListDeployments",
2883 json!({ "includeOnlyStatuses": ["Bogus"] })
2884 ))),
2885 "InvalidDeploymentStatusException"
2886 );
2887 assert_eq!(
2888 err_code(s.list_deployments(&req(
2889 "ListDeployments",
2890 json!({ "createTimeRange": { "start": 100.0, "end": 1.0 } })
2891 ))),
2892 "InvalidTimeRangeException"
2893 );
2894 let none = body_of(
2896 s.list_deployments(&req(
2897 "ListDeployments",
2898 json!({ "createTimeRange": { "start": 9_000_000_000.0_f64 } }),
2899 ))
2900 .unwrap(),
2901 );
2902 assert_eq!(none["deployments"].as_array().unwrap().len(), 0);
2903 }
2904
2905 #[test]
2908 fn list_application_revisions_filters_and_sorts() {
2909 let s = svc();
2910 s.create_application(&req(
2911 "CreateApplication",
2912 json!({ "applicationName": "ra" }),
2913 ))
2914 .unwrap();
2915 let reg = |bucket: &str| {
2916 s.register_application_revision(&req(
2917 "RegisterApplicationRevision",
2918 json!({
2919 "applicationName": "ra",
2920 "revision": { "revisionType": "S3", "s3Location": { "bucket": bucket, "key": "k", "bundleType": "zip" } }
2921 }),
2922 ))
2923 .unwrap();
2924 };
2925 reg("bucket-a");
2926 reg("bucket-b");
2927 let inc = body_of(
2929 s.list_application_revisions(&req(
2930 "ListApplicationRevisions",
2931 json!({ "applicationName": "ra", "deployed": "include" }),
2932 ))
2933 .unwrap(),
2934 );
2935 assert_eq!(inc["revisions"].as_array().unwrap().len(), 0);
2936 let exc = body_of(
2937 s.list_application_revisions(&req(
2938 "ListApplicationRevisions",
2939 json!({ "applicationName": "ra", "deployed": "exclude" }),
2940 ))
2941 .unwrap(),
2942 );
2943 assert_eq!(exc["revisions"].as_array().unwrap().len(), 2);
2944 let desc = body_of(
2946 s.list_application_revisions(&req(
2947 "ListApplicationRevisions",
2948 json!({ "applicationName": "ra", "sortBy": "registerTime", "sortOrder": "descending" }),
2949 ))
2950 .unwrap(),
2951 );
2952 assert_eq!(
2953 desc["revisions"][0]["s3Location"]["bucket"], "bucket-b",
2954 "descending should list the most-recently-registered first"
2955 );
2956 }
2957
2958 #[test]
2961 fn list_on_premises_filters_by_registration_status() {
2962 let s = svc();
2963 for n in ["i1", "i2"] {
2964 s.register_on_premises_instance(&req(
2965 "RegisterOnPremisesInstance",
2966 json!({ "instanceName": n, "iamUserArn": "arn:aws:iam::000000000000:user/u" }),
2967 ))
2968 .unwrap();
2969 }
2970 s.deregister_on_premises_instance(&req(
2971 "DeregisterOnPremisesInstance",
2972 json!({ "instanceName": "i2" }),
2973 ))
2974 .unwrap();
2975 let names = |status: &str| -> Vec<String> {
2976 body_of(
2977 s.list_on_premises_instances(&req(
2978 "ListOnPremisesInstances",
2979 json!({ "registrationStatus": status }),
2980 ))
2981 .unwrap(),
2982 )["instanceNames"]
2983 .as_array()
2984 .unwrap()
2985 .iter()
2986 .map(|v| v.as_str().unwrap().to_string())
2987 .collect()
2988 };
2989 assert_eq!(names("Registered"), vec!["i1".to_string()]);
2990 assert_eq!(names("Deregistered"), vec!["i2".to_string()]);
2991 }
2992
2993 #[test]
2996 fn add_tags_on_premises_is_all_or_nothing() {
2997 let s = svc();
2998 s.register_on_premises_instance(&req(
2999 "RegisterOnPremisesInstance",
3000 json!({ "instanceName": "ok1", "iamUserArn": "arn:aws:iam::000000000000:user/u" }),
3001 ))
3002 .unwrap();
3003 assert_eq!(
3004 err_code(s.add_tags_to_on_premises(&req(
3005 "AddTagsToOnPremisesInstances",
3006 json!({ "tags": [{ "Key": "k", "Value": "v" }], "instanceNames": ["ok1", "missing"] })
3007 ))),
3008 "InstanceNotRegisteredException"
3009 );
3010 let got = body_of(
3012 s.get_on_premises_instance(&req(
3013 "GetOnPremisesInstance",
3014 json!({ "instanceName": "ok1" }),
3015 ))
3016 .unwrap(),
3017 );
3018 assert_eq!(got["instanceInfo"]["tags"].as_array().unwrap().len(), 0);
3019 }
3020
3021 #[test]
3024 fn is_mutating_excludes_true_no_ops() {
3025 assert!(!is_mutating("ContinueDeployment"));
3026 assert!(!is_mutating("SkipWaitTimeForInstanceTermination"));
3027 assert!(!is_mutating("PutLifecycleEventHookExecutionStatus"));
3028 assert!(is_mutating("DeleteResourcesByExternalId"));
3029 }
3030
3031 #[test]
3032 fn delete_resources_by_external_id_removes_matching_deployments() {
3033 let s = svc();
3034 {
3035 let mut g = s.state.write();
3036 let st = g.get_or_create("000000000000");
3037 st.deployments.insert(
3038 "d-EXT000001".into(),
3039 json!({ "deploymentId": "d-EXT000001", "externalId": "ext-1" }),
3040 );
3041 st.deployment_order.push("d-EXT000001".into());
3042 st.deployment_settle.insert("d-EXT000001".into(), 0);
3043 }
3044 s.delete_resources_by_external_id(&req(
3045 "DeleteResourcesByExternalId",
3046 json!({ "externalId": "ext-1" }),
3047 ))
3048 .unwrap();
3049 let mut g = s.state.write();
3050 let st = g.get_or_create("000000000000");
3051 assert!(!st.deployments.contains_key("d-EXT000001"));
3052 assert!(st.deployment_order.is_empty());
3053 }
3054
3055 #[test]
3056 fn delete_application_is_idempotent() {
3057 let s = svc();
3058 s.delete_application(&req(
3059 "DeleteApplication",
3060 json!({ "applicationName": "ghost" }),
3061 ))
3062 .unwrap();
3063 }
3064
3065 fn make_app_and_group(s: &CodeDeployService, app: &str, group: &str) {
3067 s.create_application(&req("CreateApplication", json!({ "applicationName": app })))
3068 .unwrap();
3069 s.create_deployment_group(&req(
3070 "CreateDeploymentGroup",
3071 json!({ "applicationName": app, "deploymentGroupName": group, "serviceRoleArn": "arn:aws:iam::000000000000:role/cd" }),
3072 ))
3073 .unwrap();
3074 }
3075
3076 fn tags_for(s: &CodeDeployService, arn: &str) -> Vec<Value> {
3077 body_of(
3078 s.list_tags_for_resource(&req("ListTagsForResource", json!({ "ResourceArn": arn })))
3079 .unwrap(),
3080 )["Tags"]
3081 .as_array()
3082 .cloned()
3083 .unwrap_or_default()
3084 }
3085
3086 #[test]
3089 fn create_application_persists_tags() {
3090 let s = svc();
3091 s.create_application(&req(
3092 "CreateApplication",
3093 json!({ "applicationName": "tagged", "tags": [{ "Key": "env", "Value": "prod" }] }),
3094 ))
3095 .unwrap();
3096 let arn = "arn:aws:codedeploy:us-east-1:000000000000:application:tagged";
3097 let tags = tags_for(&s, arn);
3098 assert_eq!(tags.len(), 1);
3099 assert_eq!(tags[0]["Key"], "env");
3100 assert_eq!(tags[0]["Value"], "prod");
3101 }
3102
3103 #[test]
3105 fn create_deployment_group_persists_tags() {
3106 let s = svc();
3107 s.create_application(&req(
3108 "CreateApplication",
3109 json!({ "applicationName": "app" }),
3110 ))
3111 .unwrap();
3112 s.create_deployment_group(&req(
3113 "CreateDeploymentGroup",
3114 json!({
3115 "applicationName": "app",
3116 "deploymentGroupName": "dg",
3117 "serviceRoleArn": "arn:aws:iam::000000000000:role/cd",
3118 "tags": [{ "Key": "team", "Value": "core" }]
3119 }),
3120 ))
3121 .unwrap();
3122 let arn = "arn:aws:codedeploy:us-east-1:000000000000:deploymentgroup:app/dg";
3123 let tags = tags_for(&s, arn);
3124 assert_eq!(tags.len(), 1);
3125 assert_eq!(tags[0]["Key"], "team");
3126 }
3127
3128 #[test]
3131 fn create_deployment_config_validates_minimum_healthy_hosts() {
3132 let s = svc();
3133 assert_eq!(
3135 err_code(s.create_deployment_config(&req(
3136 "CreateDeploymentConfig",
3137 json!({ "deploymentConfigName": "c1", "minimumHealthyHosts": { "type": "BOGUS", "value": 1 } })
3138 ))),
3139 "InvalidMinimumHealthyHostValueException"
3140 );
3141 assert_eq!(
3143 err_code(s.create_deployment_config(&req(
3144 "CreateDeploymentConfig",
3145 json!({ "deploymentConfigName": "c2", "minimumHealthyHosts": { "type": "FLEET_PERCENT", "value": 150 } })
3146 ))),
3147 "InvalidMinimumHealthyHostValueException"
3148 );
3149 assert_eq!(
3151 err_code(s.create_deployment_config(&req(
3152 "CreateDeploymentConfig",
3153 json!({ "deploymentConfigName": "c3", "minimumHealthyHosts": { "type": "HOST_COUNT", "value": -1 } })
3154 ))),
3155 "InvalidMinimumHealthyHostValueException"
3156 );
3157 s.create_deployment_config(&req(
3159 "CreateDeploymentConfig",
3160 json!({ "deploymentConfigName": "ok", "minimumHealthyHosts": { "type": "FLEET_PERCENT", "value": 50 } }),
3161 ))
3162 .unwrap();
3163 }
3164
3165 #[test]
3168 fn deployment_group_validates_nested_enums() {
3169 let s = svc();
3170 s.create_application(&req(
3171 "CreateApplication",
3172 json!({ "applicationName": "app" }),
3173 ))
3174 .unwrap();
3175 let role = "arn:aws:iam::000000000000:role/cd";
3176 let base = |extra: Value| -> Value {
3177 let mut m = json!({
3178 "applicationName": "app",
3179 "deploymentGroupName": "dg",
3180 "serviceRoleArn": role,
3181 });
3182 for (k, v) in extra.as_object().unwrap() {
3183 m.as_object_mut().unwrap().insert(k.clone(), v.clone());
3184 }
3185 m
3186 };
3187 let cases = [
3188 (
3189 json!({ "ec2TagFilters": [{ "Key": "k", "Value": "v", "Type": "BOGUS" }] }),
3190 "InvalidEC2TagException",
3191 ),
3192 (
3193 json!({ "onPremisesInstanceTagFilters": [{ "Key": "k", "Value": "v", "Type": "BOGUS" }] }),
3194 "InvalidOnPremisesTagCombinationException",
3195 ),
3196 (
3197 json!({ "triggerConfigurations": [{ "triggerName": "t", "triggerTargetArn": "arn:aws:sns:us-east-1:000000000000:x", "triggerEvents": ["BogusEvent"] }] }),
3198 "InvalidTriggerConfigException",
3199 ),
3200 (
3201 json!({ "autoRollbackConfiguration": { "enabled": true, "events": ["NOT_AN_EVENT"] } }),
3202 "InvalidAutoRollbackConfigException",
3203 ),
3204 ];
3205 for (extra, code) in &cases {
3206 assert_eq!(
3207 err_code(
3208 s.create_deployment_group(&req("CreateDeploymentGroup", base(extra.clone())))
3209 ),
3210 *code,
3211 "create: {extra}"
3212 );
3213 }
3214 s.create_deployment_group(&req("CreateDeploymentGroup", base(json!({}))))
3216 .unwrap();
3217 assert_eq!(
3218 err_code(s.update_deployment_group(&req(
3219 "UpdateDeploymentGroup",
3220 json!({
3221 "applicationName": "app",
3222 "currentDeploymentGroupName": "dg",
3223 "ec2TagFilters": [{ "Key": "k", "Value": "v", "Type": "BOGUS" }]
3224 })
3225 ))),
3226 "InvalidEC2TagException"
3227 );
3228 s.update_deployment_group(&req(
3230 "UpdateDeploymentGroup",
3231 json!({
3232 "applicationName": "app",
3233 "currentDeploymentGroupName": "dg",
3234 "autoRollbackConfiguration": { "enabled": true, "events": ["DEPLOYMENT_FAILURE"] },
3235 "triggerConfigurations": [{ "triggerName": "t", "triggerTargetArn": "arn:aws:sns:us-east-1:000000000000:x", "triggerEvents": ["DeploymentSuccess"] }]
3236 }),
3237 ))
3238 .unwrap();
3239 }
3240
3241 #[test]
3244 fn create_deployment_requires_and_checks_group() {
3245 let s = svc();
3246 s.create_application(&req(
3247 "CreateApplication",
3248 json!({ "applicationName": "app" }),
3249 ))
3250 .unwrap();
3251 assert_eq!(
3252 err_code(s.create_deployment(&req(
3253 "CreateDeployment",
3254 json!({ "applicationName": "app" })
3255 ))),
3256 "DeploymentGroupNameRequiredException"
3257 );
3258 assert_eq!(
3259 err_code(s.create_deployment(&req(
3260 "CreateDeployment",
3261 json!({ "applicationName": "app", "deploymentGroupName": "ghost" })
3262 ))),
3263 "DeploymentGroupDoesNotExistException"
3264 );
3265 }
3266
3267 #[test]
3269 fn create_deployment_inherits_deployment_style() {
3270 let s = svc();
3271 s.create_application(&req(
3272 "CreateApplication",
3273 json!({ "applicationName": "app" }),
3274 ))
3275 .unwrap();
3276 s.create_deployment_group(&req(
3277 "CreateDeploymentGroup",
3278 json!({
3279 "applicationName": "app",
3280 "deploymentGroupName": "dg",
3281 "serviceRoleArn": "arn:aws:iam::000000000000:role/cd",
3282 "deploymentStyle": { "deploymentType": "BLUE_GREEN", "deploymentOption": "WITH_TRAFFIC_CONTROL" }
3283 }),
3284 ))
3285 .unwrap();
3286 let id = body_of(
3287 s.create_deployment(&req(
3288 "CreateDeployment",
3289 json!({ "applicationName": "app", "deploymentGroupName": "dg" }),
3290 ))
3291 .unwrap(),
3292 )["deploymentId"]
3293 .as_str()
3294 .unwrap()
3295 .to_string();
3296 let got = body_of(
3297 s.get_deployment(&req("GetDeployment", json!({ "deploymentId": id })))
3298 .unwrap(),
3299 );
3300 assert_eq!(
3301 got["deploymentInfo"]["deploymentStyle"]["deploymentType"],
3302 "BLUE_GREEN"
3303 );
3304 assert_eq!(
3305 got["deploymentInfo"]["deploymentStyle"]["deploymentOption"],
3306 "WITH_TRAFFIC_CONTROL"
3307 );
3308 }
3309
3310 #[test]
3313 fn create_deployment_carries_override_alarm_configuration() {
3314 let s = svc();
3315 make_app_and_group(&s, "app", "dg");
3316 let id = body_of(
3317 s.create_deployment(&req(
3318 "CreateDeployment",
3319 json!({
3320 "applicationName": "app",
3321 "deploymentGroupName": "dg",
3322 "overrideAlarmConfiguration": { "enabled": true, "alarms": [{ "name": "cpu-high" }] }
3323 }),
3324 ))
3325 .unwrap(),
3326 )["deploymentId"]
3327 .as_str()
3328 .unwrap()
3329 .to_string();
3330 let got = body_of(
3331 s.get_deployment(&req("GetDeployment", json!({ "deploymentId": id })))
3332 .unwrap(),
3333 );
3334 assert_eq!(
3335 got["deploymentInfo"]["overrideAlarmConfiguration"]["enabled"],
3336 true
3337 );
3338 assert_eq!(
3339 got["deploymentInfo"]["overrideAlarmConfiguration"]["alarms"][0]["name"],
3340 "cpu-high"
3341 );
3342 }
3343
3344 #[test]
3347 fn delete_purges_resource_tags() {
3348 let s = svc();
3349 s.create_application(&req(
3351 "CreateApplication",
3352 json!({ "applicationName": "app", "tags": [{ "Key": "k", "Value": "v" }] }),
3353 ))
3354 .unwrap();
3355 let app_arn = "arn:aws:codedeploy:us-east-1:000000000000:application:app";
3356 assert_eq!(tags_for(&s, app_arn).len(), 1);
3357 s.create_deployment_group(&req(
3359 "CreateDeploymentGroup",
3360 json!({
3361 "applicationName": "app",
3362 "deploymentGroupName": "dg",
3363 "serviceRoleArn": "arn:aws:iam::000000000000:role/cd",
3364 "tags": [{ "Key": "t", "Value": "core" }]
3365 }),
3366 ))
3367 .unwrap();
3368 let dg_arn = "arn:aws:codedeploy:us-east-1:000000000000:deploymentgroup:app/dg";
3369 assert_eq!(tags_for(&s, dg_arn).len(), 1);
3370 s.delete_application(&req(
3372 "DeleteApplication",
3373 json!({ "applicationName": "app" }),
3374 ))
3375 .unwrap();
3376 assert_eq!(tags_for(&s, app_arn).len(), 0);
3377 assert_eq!(tags_for(&s, dg_arn).len(), 0);
3378
3379 s.create_deployment_config(&req(
3381 "CreateDeploymentConfig",
3382 json!({ "deploymentConfigName": "cfg", "minimumHealthyHosts": { "type": "HOST_COUNT", "value": 1 } }),
3383 ))
3384 .unwrap();
3385 let cfg_arn = "arn:aws:codedeploy:us-east-1:000000000000:deploymentconfig:cfg";
3386 s.tag_resource(&req(
3387 "TagResource",
3388 json!({ "ResourceArn": cfg_arn, "Tags": [{ "Key": "k", "Value": "v" }] }),
3389 ))
3390 .unwrap();
3391 assert_eq!(tags_for(&s, cfg_arn).len(), 1);
3392 s.delete_deployment_config(&req(
3393 "DeleteDeploymentConfig",
3394 json!({ "deploymentConfigName": "cfg" }),
3395 ))
3396 .unwrap();
3397 assert_eq!(tags_for(&s, cfg_arn).len(), 0);
3398
3399 make_app_and_group(&s, "app2", "dg2");
3401 let dg2_arn = "arn:aws:codedeploy:us-east-1:000000000000:deploymentgroup:app2/dg2";
3402 s.tag_resource(&req(
3403 "TagResource",
3404 json!({ "ResourceArn": dg2_arn, "Tags": [{ "Key": "k", "Value": "v" }] }),
3405 ))
3406 .unwrap();
3407 assert_eq!(tags_for(&s, dg2_arn).len(), 1);
3408 s.delete_deployment_group(&req(
3409 "DeleteDeploymentGroup",
3410 json!({ "applicationName": "app2", "deploymentGroupName": "dg2" }),
3411 ))
3412 .unwrap();
3413 assert_eq!(tags_for(&s, dg2_arn).len(), 0);
3414 }
3415}