github.com/letsencrypt/boulder@v0.20251208.0/cmd/admin/unpause_account.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "flag" 8 "fmt" 9 "os" 10 "slices" 11 "strconv" 12 "sync" 13 "sync/atomic" 14 15 sapb "github.com/letsencrypt/boulder/sa/proto" 16 "github.com/letsencrypt/boulder/unpause" 17 ) 18 19 // subcommandUnpauseAccount encapsulates the "admin unpause-account" command. 20 type subcommandUnpauseAccount struct { 21 accountID int64 22 batchFile string 23 parallelism uint 24 } 25 26 var _ subcommand = (*subcommandUnpauseAccount)(nil) 27 28 func (u *subcommandUnpauseAccount) Desc() string { 29 return "Administratively unpause an account to allow certificate issuance attempts" 30 } 31 32 func (u *subcommandUnpauseAccount) Flags(flag *flag.FlagSet) { 33 flag.Int64Var(&u.accountID, "account", 0, "A single account ID to unpause") 34 flag.StringVar(&u.batchFile, "batch-file", "", "Path to a file containing multiple account IDs where each is separated by a newline") 35 flag.UintVar(&u.parallelism, "parallelism", 10, "The maximum number of concurrent unpause requests to send to the SA (default: 10)") 36 } 37 38 func (u *subcommandUnpauseAccount) Run(ctx context.Context, a *admin) error { 39 // This is a map of all input-selection flags to whether or not they were set 40 // to a non-default value. We use this to ensure that exactly one input 41 // selection flag was given on the command line. 42 setInputs := map[string]bool{ 43 "-account": u.accountID != 0, 44 "-batch-file": u.batchFile != "", 45 } 46 activeFlag, err := findActiveInputMethodFlag(setInputs) 47 if err != nil { 48 return err 49 } 50 51 var regIDs []int64 52 switch activeFlag { 53 case "-account": 54 regIDs = []int64{u.accountID} 55 case "-batch-file": 56 regIDs, err = a.readUnpauseAccountFile(u.batchFile) 57 default: 58 return errors.New("no recognized input method flag set (this shouldn't happen)") 59 } 60 if err != nil { 61 return fmt.Errorf("collecting serials to revoke: %w", err) 62 } 63 64 _, err = a.unpauseAccounts(ctx, regIDs, u.parallelism) 65 if err != nil { 66 return err 67 } 68 69 return nil 70 } 71 72 type unpauseCount struct { 73 accountID int64 74 count int64 75 } 76 77 // unpauseAccount concurrently unpauses all identifiers for each account using 78 // up to `parallelism` workers. It returns a count of the number of identifiers 79 // unpaused for each account and any accumulated errors. 80 func (a *admin) unpauseAccounts(ctx context.Context, accountIDs []int64, parallelism uint) ([]unpauseCount, error) { 81 if len(accountIDs) <= 0 { 82 return nil, errors.New("no account IDs provided for unpausing") 83 } 84 slices.Sort(accountIDs) 85 accountIDs = slices.Compact(accountIDs) 86 87 countChan := make(chan unpauseCount, len(accountIDs)) 88 work := make(chan int64) 89 90 var wg sync.WaitGroup 91 var errCount atomic.Uint64 92 for range parallelism { 93 wg.Go(func() { 94 for accountID := range work { 95 totalCount := int64(0) 96 for { 97 response, err := a.sac.UnpauseAccount(ctx, &sapb.RegistrationID{Id: accountID}) 98 if err != nil { 99 errCount.Add(1) 100 a.log.Errf("error unpausing accountID %d: %v", accountID, err) 101 break 102 } 103 totalCount += response.Count 104 if response.Count < unpause.RequestLimit { 105 // All identifiers have been unpaused. 106 break 107 } 108 } 109 countChan <- unpauseCount{accountID: accountID, count: totalCount} 110 } 111 }) 112 } 113 114 go func() { 115 for _, accountID := range accountIDs { 116 work <- accountID 117 } 118 close(work) 119 }() 120 121 go func() { 122 wg.Wait() 123 close(countChan) 124 }() 125 126 var unpauseCounts []unpauseCount 127 for count := range countChan { 128 unpauseCounts = append(unpauseCounts, count) 129 } 130 131 if errCount.Load() > 0 { 132 return unpauseCounts, fmt.Errorf("encountered %d errors while unpausing; see logs above for details", errCount.Load()) 133 } 134 135 return unpauseCounts, nil 136 } 137 138 // readUnpauseAccountFile parses the contents of a file containing one account 139 // ID per into a slice of int64s. It will skip malformed records and continue 140 // processing until the end of file marker. 141 func (a *admin) readUnpauseAccountFile(filePath string) ([]int64, error) { 142 fp, err := os.Open(filePath) 143 if err != nil { 144 return nil, fmt.Errorf("opening paused account data file: %w", err) 145 } 146 defer fp.Close() 147 148 var unpauseAccounts []int64 149 lineCounter := 0 150 scanner := bufio.NewScanner(fp) 151 for scanner.Scan() { 152 lineCounter++ 153 regID, err := strconv.ParseInt(scanner.Text(), 10, 64) 154 if err != nil { 155 a.log.Infof("skipping: malformed account ID entry on line %d\n", lineCounter) 156 continue 157 } 158 unpauseAccounts = append(unpauseAccounts, regID) 159 } 160 161 if err := scanner.Err(); err != nil { 162 return nil, scanner.Err() 163 } 164 165 return unpauseAccounts, nil 166 }