|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bufio" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "net/http" |
| 8 | + "net/url" |
| 9 | + "strings" |
| 10 | + |
| 11 | + "github.com/kosli-dev/cli/internal/requests" |
| 12 | + "github.com/spf13/cobra" |
| 13 | +) |
| 14 | + |
| 15 | +const deleteServiceAccountShortDesc = `Delete one or more service accounts.` |
| 16 | + |
| 17 | +const deleteServiceAccountLongDesc = deleteServiceAccountShortDesc + ` |
| 18 | +
|
| 19 | +This permanently removes the service account(s) identified by SERVICE-ACCOUNT-NAME |
| 20 | +from the organization, along with their API keys. Deletion is immediate and |
| 21 | +cannot be undone. You are asked to confirm before deletion; use |
| 22 | +^--assume-yes^/^--yes^ to skip the confirmation prompt.` |
| 23 | + |
| 24 | +const deleteServiceAccountExample = ` |
| 25 | +# delete a service account (asks for confirmation): |
| 26 | +kosli delete service-account yourServiceAccountName \ |
| 27 | + --api-token yourAPIToken \ |
| 28 | + --org yourOrgName |
| 29 | +
|
| 30 | +# delete multiple service accounts at once: |
| 31 | +kosli delete service-account sa1 sa2 \ |
| 32 | + --api-token yourAPIToken \ |
| 33 | + --org yourOrgName |
| 34 | +
|
| 35 | +# delete a service account without confirmation: |
| 36 | +kosli delete service-account yourServiceAccountName \ |
| 37 | + --assume-yes \ |
| 38 | + --api-token yourAPIToken \ |
| 39 | + --org yourOrgName |
| 40 | +` |
| 41 | + |
| 42 | +type deleteServiceAccountOptions struct { |
| 43 | + assumeYes bool |
| 44 | +} |
| 45 | + |
| 46 | +func newDeleteServiceAccountCmd(out io.Writer) *cobra.Command { |
| 47 | + o := new(deleteServiceAccountOptions) |
| 48 | + cmd := &cobra.Command{ |
| 49 | + Use: "service-account SERVICE-ACCOUNT-NAME [SERVICE-ACCOUNT-NAME...]", |
| 50 | + Aliases: []string{"sa"}, |
| 51 | + Short: deleteServiceAccountShortDesc, |
| 52 | + Long: deleteServiceAccountLongDesc, |
| 53 | + Example: deleteServiceAccountExample, |
| 54 | + Args: cobra.MinimumNArgs(1), |
| 55 | + PreRunE: func(cmd *cobra.Command, args []string) error { |
| 56 | + if err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}); err != nil { |
| 57 | + return ErrorBeforePrintingUsage(cmd, err.Error()) |
| 58 | + } |
| 59 | + return nil |
| 60 | + }, |
| 61 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 62 | + return o.run(cmd.InOrStdin(), args) |
| 63 | + }, |
| 64 | + } |
| 65 | + |
| 66 | + cmd.Flags().BoolVarP(&o.assumeYes, "assume-yes", "y", false, serviceAccountAssumeYesFlag) |
| 67 | + // keep --yes as a hidden alias for --assume-yes (bound to the same option) |
| 68 | + cmd.Flags().BoolVar(&o.assumeYes, "yes", false, serviceAccountAssumeYesFlag) |
| 69 | + if f := cmd.Flags().Lookup("yes"); f != nil { |
| 70 | + f.Hidden = true |
| 71 | + } |
| 72 | + addDryRunFlag(cmd) |
| 73 | + |
| 74 | + return cmd |
| 75 | +} |
| 76 | + |
| 77 | +func (o *deleteServiceAccountOptions) run(in io.Reader, args []string) error { |
| 78 | + if !o.assumeYes && !global.DryRun { |
| 79 | + confirmed, err := confirmServiceAccountDeletion(args, in) |
| 80 | + if err != nil { |
| 81 | + return err |
| 82 | + } |
| 83 | + if !confirmed { |
| 84 | + logger.Info("Deletion of service account(s) %s was cancelled.", strings.Join(styleServiceAccountNames(args), ", ")) |
| 85 | + return nil |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + // deletion is destructive and one-way: on any failure mid-batch, make clear |
| 90 | + // which service accounts were already deleted before it. |
| 91 | + reportAlreadyDeleted := func(i int) { |
| 92 | + if i > 0 { |
| 93 | + logger.Info("Service accounts already deleted before this failure: %s", strings.Join(styleServiceAccountNames(args[:i]), ", ")) |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + for i, name := range args { |
| 98 | + url, err := url.JoinPath(global.Host, "api/v2/service-accounts", global.Org, name) |
| 99 | + if err != nil { |
| 100 | + reportAlreadyDeleted(i) |
| 101 | + return err |
| 102 | + } |
| 103 | + |
| 104 | + reqParams := &requests.RequestParams{ |
| 105 | + Method: http.MethodDelete, |
| 106 | + URL: url, |
| 107 | + DryRun: global.DryRun, |
| 108 | + Token: global.ApiToken, |
| 109 | + } |
| 110 | + if _, err := kosliClient.Do(reqParams); err != nil { |
| 111 | + reportAlreadyDeleted(i) |
| 112 | + return fmt.Errorf("failed to delete service account: %w", err) |
| 113 | + } |
| 114 | + if !global.DryRun { |
| 115 | + logger.Info("service account %s was deleted!", style(logger.Out, name, ansiBold, ansiCyan)) |
| 116 | + } |
| 117 | + } |
| 118 | + return nil |
| 119 | +} |
| 120 | + |
| 121 | +// styleServiceAccountNames styles service account names for user-facing |
| 122 | +// messages printed via logger (bold cyan when styling is enabled). |
| 123 | +func styleServiceAccountNames(names []string) []string { |
| 124 | + styled := make([]string, len(names)) |
| 125 | + for i, name := range names { |
| 126 | + styled[i] = style(logger.Out, name, ansiBold, ansiCyan) |
| 127 | + } |
| 128 | + return styled |
| 129 | +} |
| 130 | + |
| 131 | +// confirmServiceAccountDeletion prompts the user to confirm deletion and |
| 132 | +// returns true only when the answer is an affirmative "y"/"yes" |
| 133 | +// (case-insensitive). The prompt has no trailing newline so the answer is |
| 134 | +// typed on the same line. |
| 135 | +func confirmServiceAccountDeletion(names []string, in io.Reader) (bool, error) { |
| 136 | + logger.Print("Are you sure you want to delete service account(s) %s? [y/N] ", |
| 137 | + strings.Join(styleServiceAccountNames(names), ", ")) |
| 138 | + |
| 139 | + answer, err := bufio.NewReader(in).ReadString('\n') |
| 140 | + if err != nil && err != io.EOF { |
| 141 | + return false, err |
| 142 | + } |
| 143 | + |
| 144 | + answer = strings.ToLower(strings.TrimSpace(answer)) |
| 145 | + return answer == "y" || answer == "yes", nil |
| 146 | +} |
0 commit comments