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