1use crate::executor::{GcloudExecutor, RealExecutor};
2use crate::gcloud::GcloudError;
3use propel_core::CloudRunConfig;
4use std::fmt;
5use std::path::Path;
6
7pub struct GcloudClient<E: GcloudExecutor = RealExecutor> {
9 executor: E,
10}
11
12impl GcloudClient<RealExecutor> {
13 pub fn new() -> Self {
14 Self {
15 executor: RealExecutor,
16 }
17 }
18}
19
20impl Default for GcloudClient<RealExecutor> {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl<E: GcloudExecutor> GcloudClient<E> {
27 pub fn with_executor(executor: E) -> Self {
28 Self { executor }
29 }
30
31 pub async fn check_prerequisites(
34 &self,
35 project_id: &str,
36 ) -> Result<PreflightReport, PreflightError> {
37 let mut report = PreflightReport::default();
38
39 match self
41 .executor
42 .exec(&args(["version", "--format", "value(version)"]))
43 .await
44 {
45 Ok(version) => report.gcloud_version = Some(version.trim().to_owned()),
46 Err(_) => return Err(PreflightError::GcloudNotInstalled),
47 }
48
49 match self
51 .executor
52 .exec(&args(["auth", "print-access-token", "--quiet"]))
53 .await
54 {
55 Ok(_) => report.authenticated = true,
56 Err(_) => return Err(PreflightError::NotAuthenticated),
57 }
58
59 match self
61 .executor
62 .exec(&args([
63 "projects",
64 "describe",
65 project_id,
66 "--format",
67 "value(name)",
68 ]))
69 .await
70 {
71 Ok(name) => report.project_name = Some(name.trim().to_owned()),
72 Err(_) => return Err(PreflightError::ProjectNotAccessible(project_id.to_owned())),
73 }
74
75 for api in &[
77 "cloudbuild.googleapis.com",
78 "run.googleapis.com",
79 "secretmanager.googleapis.com",
80 ] {
81 let output = self
82 .executor
83 .exec(&args([
84 "services",
85 "list",
86 "--project",
87 project_id,
88 "--filter",
89 &format!("config.name={api}"),
90 "--format",
91 "value(config.name)",
92 ]))
93 .await
94 .map_err(|e| PreflightError::ApiCheckFailed {
95 api: (*api).to_owned(),
96 source: e,
97 })?;
98
99 if output.trim().is_empty() {
100 report.disabled_apis.push((*api).to_owned());
101 }
102 }
103
104 Ok(report)
105 }
106
107 pub async fn doctor(&self, project_id: Option<&str>) -> DoctorReport {
112 let mut report = DoctorReport::default();
113
114 match self.executor.exec(&args(["version"])).await {
116 Ok(v) => {
117 let version = v
119 .lines()
120 .next()
121 .and_then(|line| line.strip_prefix("Google Cloud SDK "))
122 .unwrap_or(v.trim());
124 report.gcloud = CheckResult::ok(version.trim());
125 }
126 Err(e) => report.gcloud = CheckResult::fail(&e.to_string()),
127 }
128
129 match self
131 .executor
132 .exec(&args(["config", "get-value", "account"]))
133 .await
134 {
135 Ok(a) if !a.trim().is_empty() => report.account = CheckResult::ok(a.trim()),
136 _ => report.account = CheckResult::fail("no active account"),
137 }
138
139 let Some(pid) = project_id else {
141 report.project = CheckResult::fail("gcp_project_id not set in propel.toml");
142 return report;
143 };
144
145 match self
146 .executor
147 .exec(&args([
148 "projects",
149 "describe",
150 pid,
151 "--format",
152 "value(name)",
153 ]))
154 .await
155 {
156 Ok(name) => {
157 report.project = CheckResult::ok(&format!("{pid} ({name})", name = name.trim()))
158 }
159 Err(_) => {
160 report.project = CheckResult::fail(&format!("{pid} — not accessible"));
161 return report;
162 }
163 }
164
165 match self
167 .executor
168 .exec(&args([
169 "billing",
170 "projects",
171 "describe",
172 pid,
173 "--format",
174 "value(billingEnabled)",
175 ]))
176 .await
177 {
178 Ok(v) if v.trim().eq_ignore_ascii_case("true") => {
179 report.billing = CheckResult::ok("Enabled");
180 }
181 _ => report.billing = CheckResult::fail("Billing not enabled"),
182 }
183
184 let required_apis = [
186 ("Cloud Build", "cloudbuild.googleapis.com"),
187 ("Cloud Run", "run.googleapis.com"),
188 ("Secret Manager", "secretmanager.googleapis.com"),
189 ("Artifact Registry", "artifactregistry.googleapis.com"),
190 ];
191
192 for (label, api) in &required_apis {
193 let result = self
194 .executor
195 .exec(&args([
196 "services",
197 "list",
198 "--project",
199 pid,
200 "--filter",
201 &format!("config.name={api}"),
202 "--format",
203 "value(config.name)",
204 ]))
205 .await;
206
207 let check = match result {
208 Ok(out) if !out.trim().is_empty() => CheckResult::ok("Enabled"),
209 Ok(_) => CheckResult::fail("Not enabled"),
210 Err(e) => CheckResult::fail(&format!("Check failed: {e}")),
211 };
212
213 report.apis.push(ApiCheck {
214 name: label.to_string(),
215 result: check,
216 });
217 }
218
219 report
220 }
221
222 pub async fn ensure_artifact_repo(
226 &self,
227 project_id: &str,
228 region: &str,
229 repo_name: &str,
230 ) -> Result<(), DeployError> {
231 let exists = self
232 .executor
233 .exec(&args([
234 "artifacts",
235 "repositories",
236 "describe",
237 repo_name,
238 "--project",
239 project_id,
240 "--location",
241 region,
242 ]))
243 .await
244 .is_ok();
245
246 if !exists {
247 self.executor
248 .exec(&args([
249 "artifacts",
250 "repositories",
251 "create",
252 repo_name,
253 "--project",
254 project_id,
255 "--location",
256 region,
257 "--repository-format",
258 "docker",
259 "--quiet",
260 ]))
261 .await
262 .map_err(|e| DeployError::Deploy { source: e })?;
263 }
264
265 Ok(())
266 }
267
268 pub async fn delete_image(&self, image_tag: &str, project_id: &str) -> Result<(), DeployError> {
270 self.executor
271 .exec(&args([
272 "artifacts",
273 "docker",
274 "images",
275 "delete",
276 image_tag,
277 "--project",
278 project_id,
279 "--delete-tags",
280 "--quiet",
281 ]))
282 .await
283 .map_err(|e| DeployError::Deploy { source: e })?;
284
285 Ok(())
286 }
287
288 pub async fn submit_build(
292 &self,
293 bundle_dir: &Path,
294 project_id: &str,
295 image_tag: &str,
296 ) -> Result<(), CloudBuildError> {
297 let bundle_str = bundle_dir
298 .to_str()
299 .ok_or_else(|| CloudBuildError::InvalidPath(bundle_dir.to_path_buf()))?;
300
301 self.executor
302 .exec_streaming(&args([
303 "builds",
304 "submit",
305 bundle_str,
306 "--project",
307 project_id,
308 "--tag",
309 image_tag,
310 "--quiet",
311 ]))
312 .await
313 .map_err(|e| CloudBuildError::Submit { source: e })
314 }
315
316 pub async fn submit_build_captured(
318 &self,
319 bundle_dir: &Path,
320 project_id: &str,
321 image_tag: &str,
322 ) -> Result<String, CloudBuildError> {
323 let bundle_str = bundle_dir
324 .to_str()
325 .ok_or_else(|| CloudBuildError::InvalidPath(bundle_dir.to_path_buf()))?;
326
327 self.executor
328 .exec(&args([
329 "builds",
330 "submit",
331 bundle_str,
332 "--project",
333 project_id,
334 "--tag",
335 image_tag,
336 "--quiet",
337 ]))
338 .await
339 .map_err(|e| CloudBuildError::Submit { source: e })
340 }
341
342 pub async fn deploy_to_cloud_run(
345 &self,
346 service_name: &str,
347 image_tag: &str,
348 project_id: &str,
349 region: &str,
350 config: &CloudRunConfig,
351 secrets: &[String],
352 ) -> Result<String, DeployError> {
353 let cpu = config.cpu.to_string();
354 let min = config.min_instances.to_string();
355 let max = config.max_instances.to_string();
356 let concurrency = config.concurrency.to_string();
357 let port = config.port.to_string();
358
359 let secrets_flag = secrets
361 .iter()
362 .map(|s| format!("{s}={s}:latest"))
363 .collect::<Vec<_>>()
364 .join(",");
365
366 let mut cmd = vec![
367 "run",
368 "deploy",
369 service_name,
370 "--image",
371 image_tag,
372 "--project",
373 project_id,
374 "--region",
375 region,
376 "--platform",
377 "managed",
378 "--memory",
379 &config.memory,
380 "--cpu",
381 &cpu,
382 "--min-instances",
383 &min,
384 "--max-instances",
385 &max,
386 "--concurrency",
387 &concurrency,
388 "--port",
389 &port,
390 "--allow-unauthenticated",
391 "--quiet",
392 "--format",
393 "value(status.url)",
394 ];
395
396 if !secrets_flag.is_empty() {
397 cmd.push("--update-secrets");
398 cmd.push(&secrets_flag);
399 }
400
401 let cmd_owned: Vec<String> = cmd.iter().map(|s| (*s).to_owned()).collect();
402
403 let output = self
404 .executor
405 .exec(&cmd_owned)
406 .await
407 .map_err(|e| DeployError::Deploy { source: e })?;
408
409 Ok(output.trim().to_owned())
410 }
411
412 pub async fn describe_service(
413 &self,
414 service_name: &str,
415 project_id: &str,
416 region: &str,
417 ) -> Result<String, DeployError> {
418 self.executor
419 .exec(&args([
420 "run",
421 "services",
422 "describe",
423 service_name,
424 "--project",
425 project_id,
426 "--region",
427 region,
428 "--format",
429 "yaml(status)",
430 ]))
431 .await
432 .map_err(|e| DeployError::Deploy { source: e })
433 }
434
435 pub async fn delete_service(
436 &self,
437 service_name: &str,
438 project_id: &str,
439 region: &str,
440 ) -> Result<(), DeployError> {
441 self.executor
442 .exec(&args([
443 "run",
444 "services",
445 "delete",
446 service_name,
447 "--project",
448 project_id,
449 "--region",
450 region,
451 "--quiet",
452 ]))
453 .await
454 .map_err(|e| DeployError::Deploy { source: e })?;
455
456 Ok(())
457 }
458
459 pub async fn read_logs(
461 &self,
462 service_name: &str,
463 project_id: &str,
464 region: &str,
465 limit: u32,
466 ) -> Result<(), DeployError> {
467 let limit_str = limit.to_string();
468 self.executor
469 .exec_streaming(&args([
470 "run",
471 "services",
472 "logs",
473 "read",
474 service_name,
475 "--project",
476 project_id,
477 "--region",
478 region,
479 "--limit",
480 &limit_str,
481 ]))
482 .await
483 .map_err(|e| DeployError::Logs { source: e })
484 }
485
486 pub async fn read_logs_captured(
488 &self,
489 service_name: &str,
490 project_id: &str,
491 region: &str,
492 limit: u32,
493 ) -> Result<String, DeployError> {
494 let limit_str = limit.to_string();
495 self.executor
496 .exec(&args([
497 "run",
498 "services",
499 "logs",
500 "read",
501 service_name,
502 "--project",
503 project_id,
504 "--region",
505 region,
506 "--limit",
507 &limit_str,
508 ]))
509 .await
510 .map_err(|e| DeployError::Logs { source: e })
511 }
512
513 pub async fn tail_logs(
514 &self,
515 service_name: &str,
516 project_id: &str,
517 region: &str,
518 ) -> Result<(), DeployError> {
519 self.executor
520 .exec_streaming(&args([
521 "run",
522 "services",
523 "logs",
524 "tail",
525 service_name,
526 "--project",
527 project_id,
528 "--region",
529 region,
530 ]))
531 .await
532 .map_err(|e| DeployError::Logs { source: e })
533 }
534
535 pub async fn set_secret(
538 &self,
539 project_id: &str,
540 secret_name: &str,
541 secret_value: &str,
542 ) -> Result<(), SecretError> {
543 let secret_exists = self
544 .executor
545 .exec(&args([
546 "secrets",
547 "describe",
548 secret_name,
549 "--project",
550 project_id,
551 ]))
552 .await
553 .is_ok();
554
555 if !secret_exists {
556 self.executor
557 .exec(&args([
558 "secrets",
559 "create",
560 secret_name,
561 "--project",
562 project_id,
563 "--replication-policy",
564 "automatic",
565 ]))
566 .await
567 .map_err(|e| SecretError::Create { source: e })?;
568 }
569
570 self.executor
571 .exec_with_stdin(
572 &args([
573 "secrets",
574 "versions",
575 "add",
576 secret_name,
577 "--project",
578 project_id,
579 "--data-file",
580 "-",
581 ]),
582 secret_value.as_bytes(),
583 )
584 .await
585 .map_err(|e| SecretError::AddVersion { source: e })?;
586
587 Ok(())
588 }
589
590 pub async fn get_project_number(&self, project_id: &str) -> Result<String, DeployError> {
591 let output = self
592 .executor
593 .exec(&args([
594 "projects",
595 "describe",
596 project_id,
597 "--format",
598 "value(projectNumber)",
599 ]))
600 .await
601 .map_err(|e| DeployError::Deploy { source: e })?;
602
603 Ok(output.trim().to_owned())
604 }
605
606 pub async fn grant_secret_access(
607 &self,
608 project_id: &str,
609 secret_name: &str,
610 service_account: &str,
611 ) -> Result<(), SecretError> {
612 let member = format!("serviceAccount:{service_account}");
613 self.executor
614 .exec(&args([
615 "secrets",
616 "add-iam-policy-binding",
617 secret_name,
618 "--project",
619 project_id,
620 "--member",
621 &member,
622 "--role",
623 "roles/secretmanager.secretAccessor",
624 ]))
625 .await
626 .map_err(|e| SecretError::GrantAccess { source: e })?;
627
628 Ok(())
629 }
630
631 pub async fn revoke_secret_access(
632 &self,
633 project_id: &str,
634 secret_name: &str,
635 service_account: &str,
636 ) -> Result<(), SecretError> {
637 let member = format!("serviceAccount:{service_account}");
638 self.executor
639 .exec(&args([
640 "secrets",
641 "remove-iam-policy-binding",
642 secret_name,
643 "--project",
644 project_id,
645 "--member",
646 &member,
647 "--role",
648 "roles/secretmanager.secretAccessor",
649 ]))
650 .await
651 .map_err(|e| SecretError::RevokeAccess { source: e })?;
652
653 Ok(())
654 }
655
656 pub async fn list_secrets(&self, project_id: &str) -> Result<Vec<String>, SecretError> {
657 let output = self
658 .executor
659 .exec(&args([
660 "secrets",
661 "list",
662 "--project",
663 project_id,
664 "--format",
665 "value(name)",
666 ]))
667 .await
668 .map_err(|e| SecretError::List { source: e })?;
669
670 Ok(output.lines().map(|s| s.to_owned()).collect())
671 }
672
673 pub async fn delete_secret(
674 &self,
675 project_id: &str,
676 secret_name: &str,
677 ) -> Result<(), SecretError> {
678 self.executor
679 .exec(&args([
680 "secrets",
681 "delete",
682 secret_name,
683 "--project",
684 project_id,
685 "--quiet",
686 ]))
687 .await
688 .map_err(|e| SecretError::Delete { source: e })?;
689
690 Ok(())
691 }
692
693 pub async fn ensure_wif_pool(&self, project_id: &str, pool_id: &str) -> Result<bool, WifError> {
698 match self
699 .executor
700 .exec(&args([
701 "iam",
702 "workload-identity-pools",
703 "create",
704 pool_id,
705 "--project",
706 project_id,
707 "--location",
708 "global",
709 "--display-name",
710 "Propel GitHub Actions",
711 ]))
712 .await
713 {
714 Ok(_) => Ok(true),
715 Err(ref e) if is_already_exists(e) => Ok(false),
716 Err(e) => Err(WifError::CreatePool { source: e }),
717 }
718 }
719
720 pub async fn ensure_oidc_provider(
723 &self,
724 project_id: &str,
725 pool_id: &str,
726 provider_id: &str,
727 github_repo: &str,
728 ) -> Result<bool, WifError> {
729 let attribute_condition = format!("assertion.repository == '{github_repo}'");
730
731 let cmd: Vec<String> = [
732 "iam",
733 "workload-identity-pools",
734 "providers",
735 "create-oidc",
736 provider_id,
737 "--project",
738 project_id,
739 "--location",
740 "global",
741 "--workload-identity-pool",
742 pool_id,
743 "--display-name",
744 "GitHub",
745 "--attribute-mapping",
746 "google.subject=assertion.sub,attribute.repository=assertion.repository",
747 "--attribute-condition",
748 &attribute_condition,
749 "--issuer-uri",
750 "https://token.actions.githubusercontent.com",
751 ]
752 .iter()
753 .map(|s| (*s).to_owned())
754 .collect();
755
756 match self.executor.exec(&cmd).await {
757 Ok(_) => Ok(true),
758 Err(ref e) if is_already_exists(e) => Ok(false),
759 Err(e) => Err(WifError::CreateProvider { source: e }),
760 }
761 }
762
763 pub async fn ensure_service_account(
766 &self,
767 project_id: &str,
768 sa_id: &str,
769 display_name: &str,
770 ) -> Result<bool, WifError> {
771 match self
772 .executor
773 .exec(&args([
774 "iam",
775 "service-accounts",
776 "create",
777 sa_id,
778 "--project",
779 project_id,
780 "--display-name",
781 display_name,
782 ]))
783 .await
784 {
785 Ok(_) => Ok(true),
786 Err(ref e) if is_already_exists(e) => Ok(false),
787 Err(e) => Err(WifError::CreateServiceAccount { source: e }),
788 }
789 }
790
791 pub async fn bind_iam_roles(
793 &self,
794 project_id: &str,
795 sa_email: &str,
796 roles: &[&str],
797 ) -> Result<(), WifError> {
798 let member = format!("serviceAccount:{sa_email}");
799 for role in roles {
800 self.executor
801 .exec(&args([
802 "projects",
803 "add-iam-policy-binding",
804 project_id,
805 "--member",
806 &member,
807 "--role",
808 role,
809 "--quiet",
810 ]))
811 .await
812 .map_err(|e| WifError::BindRole {
813 role: (*role).to_owned(),
814 source: e,
815 })?;
816 }
817
818 Ok(())
819 }
820
821 pub async fn bind_wif_to_sa(
823 &self,
824 project_id: &str,
825 project_number: &str,
826 pool_id: &str,
827 sa_email: &str,
828 github_repo: &str,
829 ) -> Result<(), WifError> {
830 let member = format!(
831 "principalSet://iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/attribute.repository/{github_repo}"
832 );
833
834 self.executor
835 .exec(&args([
836 "iam",
837 "service-accounts",
838 "add-iam-policy-binding",
839 sa_email,
840 "--project",
841 project_id,
842 "--role",
843 "roles/iam.workloadIdentityUser",
844 "--member",
845 &member,
846 ]))
847 .await
848 .map_err(|e| WifError::BindWif { source: e })?;
849
850 Ok(())
851 }
852
853 pub async fn delete_wif_pool(&self, project_id: &str, pool_id: &str) -> Result<(), WifError> {
855 self.executor
856 .exec(&args([
857 "iam",
858 "workload-identity-pools",
859 "delete",
860 pool_id,
861 "--project",
862 project_id,
863 "--location",
864 "global",
865 "--quiet",
866 ]))
867 .await
868 .map_err(|e| WifError::DeletePool { source: e })?;
869
870 Ok(())
871 }
872
873 pub async fn delete_service_account(
875 &self,
876 project_id: &str,
877 sa_email: &str,
878 ) -> Result<(), WifError> {
879 self.executor
880 .exec(&args([
881 "iam",
882 "service-accounts",
883 "delete",
884 sa_email,
885 "--project",
886 project_id,
887 "--quiet",
888 ]))
889 .await
890 .map_err(|e| WifError::DeleteServiceAccount { source: e })?;
891
892 Ok(())
893 }
894}
895
896fn args<const N: usize>(a: [&str; N]) -> Vec<String> {
899 a.iter().map(|s| (*s).to_owned()).collect()
900}
901
902fn is_already_exists(e: &GcloudError) -> bool {
904 match e {
905 GcloudError::CommandFailed { stderr, .. } => {
906 stderr.contains("ALREADY_EXISTS") || stderr.contains("already exists")
907 }
908 _ => false,
909 }
910}
911
912#[derive(Debug, Default)]
915pub struct PreflightReport {
916 pub gcloud_version: Option<String>,
917 pub authenticated: bool,
918 pub project_name: Option<String>,
919 pub disabled_apis: Vec<String>,
920}
921
922impl PreflightReport {
923 pub fn has_warnings(&self) -> bool {
924 !self.disabled_apis.is_empty()
925 }
926}
927
928#[derive(Debug, thiserror::Error)]
929pub enum PreflightError {
930 #[error("gcloud CLI not installed — https://cloud.google.com/sdk/docs/install")]
931 GcloudNotInstalled,
932
933 #[error("not authenticated — run: gcloud auth login")]
934 NotAuthenticated,
935
936 #[error("GCP project '{0}' is not accessible — check project ID and permissions")]
937 ProjectNotAccessible(String),
938
939 #[error("failed to check API status for {api}")]
940 ApiCheckFailed { api: String, source: GcloudError },
941}
942
943#[derive(Debug, Default)]
946pub struct DoctorReport {
947 pub gcloud: CheckResult,
948 pub account: CheckResult,
949 pub project: CheckResult,
950 pub billing: CheckResult,
951 pub apis: Vec<ApiCheck>,
952 pub config_file: CheckResult,
953}
954
955impl DoctorReport {
956 pub fn all_passed(&self) -> bool {
957 self.gcloud.passed
958 && self.account.passed
959 && self.project.passed
960 && self.billing.passed
961 && self.config_file.passed
962 && self.apis.iter().all(|a| a.result.passed)
963 }
964}
965
966impl fmt::Display for DoctorReport {
967 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
968 writeln!(f, "Propel Doctor")?;
969 writeln!(f, "------------------------------")?;
970
971 let rows: [(&str, &CheckResult); 4] = [
972 ("gcloud CLI", &self.gcloud),
973 ("Authentication", &self.account),
974 ("GCP Project", &self.project),
975 ("Billing", &self.billing),
976 ];
977
978 for (label, result) in &rows {
979 writeln!(f, "{:<22}{:<4}{}", label, result.icon(), result.detail)?;
980 }
981
982 for api in &self.apis {
983 writeln!(
984 f,
985 "{:<22}{:<4}{}",
986 format!("{} API", api.name),
987 api.result.icon(),
988 api.result.detail,
989 )?;
990 }
991
992 writeln!(
993 f,
994 "{:<22}{:<4}{}",
995 "propel.toml",
996 self.config_file.icon(),
997 self.config_file.detail,
998 )?;
999
1000 writeln!(f, "------------------------------")?;
1001 if self.all_passed() {
1002 write!(f, "All checks passed!")?;
1003 } else {
1004 write!(f, "Some checks failed — see above for details")?;
1005 }
1006
1007 Ok(())
1008 }
1009}
1010
1011#[derive(Debug, Default, Clone)]
1012pub struct CheckResult {
1013 pub passed: bool,
1014 pub detail: String,
1015}
1016
1017impl CheckResult {
1018 pub fn ok(detail: &str) -> Self {
1019 Self {
1020 passed: true,
1021 detail: detail.to_owned(),
1022 }
1023 }
1024
1025 pub fn fail(detail: &str) -> Self {
1026 Self {
1027 passed: false,
1028 detail: detail.to_owned(),
1029 }
1030 }
1031
1032 pub fn icon(&self) -> &'static str {
1033 if self.passed { "OK" } else { "NG" }
1034 }
1035}
1036
1037#[derive(Debug, Clone)]
1038pub struct ApiCheck {
1039 pub name: String,
1040 pub result: CheckResult,
1041}
1042
1043#[derive(Debug, thiserror::Error)]
1044pub enum CloudBuildError {
1045 #[error("bundle path is not valid UTF-8: {0}")]
1046 InvalidPath(std::path::PathBuf),
1047
1048 #[error("cloud build submission failed")]
1049 Submit { source: GcloudError },
1050}
1051
1052#[derive(Debug, thiserror::Error)]
1053pub enum DeployError {
1054 #[error("cloud run deployment failed")]
1055 Deploy { source: GcloudError },
1056
1057 #[error("failed to read logs")]
1058 Logs { source: GcloudError },
1059}
1060
1061#[derive(Debug, thiserror::Error)]
1062pub enum SecretError {
1063 #[error("failed to create secret")]
1064 Create { source: GcloudError },
1065
1066 #[error("failed to add secret version")]
1067 AddVersion { source: GcloudError },
1068
1069 #[error("failed to list secrets")]
1070 List { source: GcloudError },
1071
1072 #[error("failed to grant secret access")]
1073 GrantAccess { source: GcloudError },
1074
1075 #[error("failed to revoke secret access")]
1076 RevokeAccess { source: GcloudError },
1077
1078 #[error("failed to delete secret")]
1079 Delete { source: GcloudError },
1080}
1081
1082#[derive(Debug, thiserror::Error)]
1083pub enum WifError {
1084 #[error("failed to create workload identity pool")]
1085 CreatePool { source: GcloudError },
1086
1087 #[error("failed to create OIDC provider")]
1088 CreateProvider { source: GcloudError },
1089
1090 #[error("failed to create service account")]
1091 CreateServiceAccount { source: GcloudError },
1092
1093 #[error("failed to bind IAM role: {role}")]
1094 BindRole { role: String, source: GcloudError },
1095
1096 #[error("failed to bind WIF to service account")]
1097 BindWif { source: GcloudError },
1098
1099 #[error("failed to delete workload identity pool")]
1100 DeletePool { source: GcloudError },
1101
1102 #[error("failed to delete service account")]
1103 DeleteServiceAccount { source: GcloudError },
1104}