diff --git a/api/api.go b/api/api.go index f54b466..ec9e667 100644 --- a/api/api.go +++ b/api/api.go @@ -13,10 +13,10 @@ var ErrBadStatusCode = fmt.Errorf("bad status code") type Client struct { BaseURL string - token string + Token string - basicAuthUser string - basicAuthPassword string + BasicAuthUser string + BasicAuthPassword string httpClient *http.Client } @@ -31,11 +31,11 @@ func WithAuthentication(token string) ClientOption { return func(cl *Client) { auth := strings.SplitN(token, ":", 2) if len(auth) == 2 { - cl.basicAuthUser = auth[0] - cl.basicAuthPassword = auth[1] + cl.BasicAuthUser = auth[0] + cl.BasicAuthPassword = auth[1] return } - cl.token = token + cl.Token = token } } @@ -73,10 +73,10 @@ func (cl Client) newRequest(ctx context.Context, method, url string) (*http.Requ // There is two cases, either we have provided a service account's Token or // the basicAuth. As the token is the recommended way to interact with the // API let's use it first - if cl.token != "" { - req.Header.Add("Authorization", "Bearer "+cl.token) - } else if cl.basicAuthUser != "" && cl.basicAuthPassword != "" { - req.SetBasicAuth(cl.basicAuthUser, cl.basicAuthPassword) + if cl.Token != "" { + req.Header.Add("Authorization", "Bearer "+cl.Token) + } else if cl.BasicAuthUser != "" && cl.BasicAuthPassword != "" { + req.SetBasicAuth(cl.BasicAuthUser, cl.BasicAuthPassword) } return req, err } diff --git a/api/grafana/grafana.go b/api/grafana/grafana.go index 953bd7c..6261369 100644 --- a/api/grafana/grafana.go +++ b/api/grafana/grafana.go @@ -77,6 +77,12 @@ func (cl APIClient) GetOrgs(ctx context.Context) ([]Org, error) { return orgs, err } +func (cl APIClient) GetCurrentOrg(ctx context.Context) (Org, error) { + var org Org + err := cl.Request(ctx, http.MethodGet, "org", &org) + return org, err +} + func (cl APIClient) UserSwitchContext(ctx context.Context, orgID string) error { return cl.Request(ctx, http.MethodPost, "user/using/"+orgID, nil) } diff --git a/detector/detector.go b/detector/detector.go index ad6a0c7..447d7d7 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "strconv" "strings" "github.com/grafana/detect-angular-dashboards/api/gcom" @@ -19,7 +20,7 @@ const ( ) // Run runs the angular detector tool against the specified Grafana instance. -func Run(ctx context.Context, log *logger.LeveledLogger, grafanaClient grafana.APIClient) ([]output.Dashboard, error) { +func Run(ctx context.Context, log *logger.LeveledLogger, grafanaClient grafana.APIClient, orgID int) ([]output.Dashboard, error) { var ( finalOutput []output.Dashboard // Determine if we should use GCOM or frontendsettings @@ -125,9 +126,14 @@ func Run(ctx context.Context, log *logger.LeveledLogger, grafanaClient grafana.A return []output.Dashboard{}, fmt.Errorf("get dashboards: %w", err) } + orgIDURLsuffix := "?" + url.Values{ + "orgID": []string{strconv.Itoa(orgID)}, + }.Encode() + for _, d := range dashboards { // Determine absolute dashboard URL for output - dashboardAbsURL, err := url.JoinPath(strings.TrimSuffix(grafanaClient.BaseURL, "/api"), d.URL) + + dashboardAbsURL, err := url.JoinPath(strings.TrimSuffix(grafanaClient.BaseURL, "/api"), d.URL+orgIDURLsuffix) if err != nil { // Silently ignore errors dashboardAbsURL = "" diff --git a/main.go b/main.go index 351af41..b92ef0e 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,15 @@ package main import ( + "cmp" "context" "crypto/tls" "flag" "fmt" "net/http" "os" + "slices" + "strconv" "github.com/grafana/detect-angular-dashboards/api" "github.com/grafana/detect-angular-dashboards/api/grafana" @@ -43,6 +46,7 @@ func main() { verboseFlag := flag.Bool("v", false, "verbose output") jsonOutputFlag := flag.Bool("j", false, "json output") skipTLSFlag := flag.Bool("insecure", false, "skip TLS verification") + bulkDetectionFlag := flag.Bool("bulk", false, "detect use of angular in all orgs, requires basicauth instead of token") flag.Parse() if *versionFlag { @@ -62,6 +66,10 @@ func main() { if flag.NArg() >= 1 { grafanaURL = flag.Arg(0) } + var ( + orgs []grafana.Org + currentOrg grafana.Org + ) log.Log("Detecting Angular dashboards for %q", grafanaURL) @@ -74,10 +82,48 @@ func main() { })) } client := grafana.NewAPIClient(api.NewClient(grafanaURL, opts...)) - finalOutput, err := detector.Run(ctx, log, client) + currentOrg, err = client.GetCurrentOrg(ctx) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(0) + _, _ = fmt.Fprintf(os.Stderr, "failed to get current org: %s\n", err) + os.Exit(1) + } + + // we can't do bulk detection with token + if *bulkDetectionFlag && client.BasicAuthUser != "" { + orgs, err = client.GetOrgs(ctx) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to get org: %s\n", err) + os.Exit(1) + } + log.Log("Fount %d orgs to scan\n", len(orgs)) + slices.SortFunc(orgs, func(a, b grafana.Org) int { + return cmp.Compare(a.ID, b.ID) + }) + } else { + orgs = append(orgs, currentOrg) + } + + finalOutput := []output.Dashboard{} + orgsFinalOutput := map[int][]output.Dashboard{} + + for _, org := range orgs { + // we can only switch org with basicauth + if client.BasicAuthUser != "" { + log.Log("Detecting Angular dashboards for org: %s(%d)\n", org.Name, org.ID) + err = client.UserSwitchContext(ctx, strconv.Itoa(org.ID)) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to switch to org: %s\n", err) + continue + } + } + + summary, err := detector.Run(ctx, log, client, org.ID) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to scan org %d: %s\n", org.ID, err) + os.Exit(0) + } + orgsFinalOutput[org.ID] = summary + finalOutput = append(finalOutput, summary...) } var out output.Outputter @@ -88,7 +134,21 @@ func main() { } // Print output - if err := out.Output(finalOutput); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "output: %s\n", err) + if *bulkDetectionFlag { + if err := out.BulkOutput(orgsFinalOutput); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "output: %s\n", err) + } + } else { + if err := out.Output(finalOutput); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "output: %s\n", err) + } + } + + // switch back to initial org + if client.BasicAuthUser != "" { + err = client.UserSwitchContext(ctx, strconv.Itoa(currentOrg.ID)) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to switch back to initial org: %s\n", err) + } } } diff --git a/output/output.go b/output/output.go index fa8aea8..4ff1484 100644 --- a/output/output.go +++ b/output/output.go @@ -57,6 +57,7 @@ type Dashboard struct { type Outputter interface { Output([]Dashboard) error + BulkOutput(map[int][]Dashboard) error } type LoggerReadableOutput struct { @@ -81,6 +82,19 @@ func (o LoggerReadableOutput) Output(v []Dashboard) error { return nil } +func (o LoggerReadableOutput) BulkOutput(v map[int][]Dashboard) error { + for org, dashboards := range v { + if len(dashboards) > 0 { + o.log.Log("Found dashboards with Angular plugins in org %d", org) + err := o.Output(dashboards) + if err != nil { + return err + } + } + } + return nil +} + type JSONOutputter struct { writer io.Writer } @@ -90,6 +104,12 @@ func NewJSONOutputter(w io.Writer) JSONOutputter { } func (o JSONOutputter) Output(v []Dashboard) error { + enc := json.NewEncoder(o.writer) + enc.SetIndent("", " ") + return enc.Encode(o.removeDashboardsWithoutDetections(v)) +} + +func (o JSONOutputter) removeDashboardsWithoutDetections(v []Dashboard) []Dashboard { var j int for i, dashboard := range v { // Remove dashboards without detections @@ -99,7 +119,17 @@ func (o JSONOutputter) Output(v []Dashboard) error { v[j] = v[i] j++ } - v = v[:j] + return v[:j] +} + +func (o JSONOutputter) BulkOutput(v map[int][]Dashboard) error { + for orgID, dashboards := range v { + if len(dashboards) == 0 { + delete(v, orgID) + } else { + v[orgID] = o.removeDashboardsWithoutDetections(dashboards) + } + } enc := json.NewEncoder(o.writer) enc.SetIndent("", " ") return enc.Encode(v)