Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ All notable changes to this project will be documented in this file.
- `--provider`/`-p` flag on `grant revoke --all` and interactive mode to filter by cloud provider
- Session ID displayed in `grant status` output for easy reference with `grant revoke`

### Changed

- `grant status` now fetches sessions and eligibility data concurrently, reducing wall-clock time by ~2s

### Fixed

- `grant revoke` now rejects `--provider` in direct mode (session IDs are already explicit)
Expand Down
87 changes: 87 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,97 @@ import (
"context"
"fmt"
"io"
"sync"

scamodels "github.com/aaearon/grant-cli/internal/sca/models"
)

// statusData holds the results of concurrent sessions + eligibility fetches.
type statusData struct {
sessions *scamodels.SessionsResponse
nameMap map[string]string
}

// fetchStatusData fires sessions and all-CSP eligibility calls concurrently,
// then joins results. A sessions error is fatal; eligibility errors are
// gracefully degraded (empty nameMap entry, verbose warning).
func fetchStatusData(
ctx context.Context,
sessionLister sessionLister,
eligLister eligibilityLister,
cspFilter *scamodels.CSP,
errWriter io.Writer,
) (*statusData, error) {
type eligResult struct {
csp scamodels.CSP
targets []scamodels.EligibleTarget
err error
}

var (
sessions *scamodels.SessionsResponse
sessionsErr error
wg sync.WaitGroup
)

// Goroutine 1: fetch sessions
wg.Add(1)
go func() {
defer wg.Done()
sessions, sessionsErr = sessionLister.ListSessions(ctx, cspFilter)
}()

// Determine which CSPs to query for eligibility
cspsToQuery := supportedCSPs
if cspFilter != nil {
cspsToQuery = []scamodels.CSP{*cspFilter}
}

// Goroutines 2..N: fetch eligibility for each CSP
eligResults := make(chan eligResult, len(cspsToQuery))
for _, csp := range cspsToQuery {
wg.Add(1)
go func(csp scamodels.CSP) {
defer wg.Done()
resp, err := eligLister.ListEligibility(ctx, csp)
if err != nil || resp == nil {
eligResults <- eligResult{csp: csp, err: err}
return
}
eligResults <- eligResult{csp: csp, targets: resp.Response}
}(csp)
}

// Close channel after all goroutines finish
go func() {
wg.Wait()
close(eligResults)
}()

// Build nameMap from eligibility results
nameMap := make(map[string]string)
for r := range eligResults {
if r.err != nil {
if verbose {
fmt.Fprintf(errWriter, "Warning: failed to fetch names for %s: %v\n", r.csp, r.err)
}
continue
}
for _, t := range r.targets {
if t.WorkspaceName != "" {
nameMap[t.WorkspaceID] = t.WorkspaceName
}
}
}

// Check sessions result (goroutine has finished since channel is drained after wg.Wait)
if sessionsErr != nil {
return nil, fmt.Errorf("failed to list sessions: %w", sessionsErr)
}

return &statusData{sessions: sessions, nameMap: nameMap}, nil
}

// buildWorkspaceNameMap fetches eligibility for each unique CSP in sessions
// and builds a workspaceID -> workspaceName map. Errors are silently ignored
// (graceful degradation — the raw workspace ID is shown as fallback).
Expand Down
Loading