Skip to content
Open
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
20 changes: 10 additions & 10 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions api/grafana/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
10 changes: 8 additions & 2 deletions detector/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/url"
"strconv"
"strings"

"github.com/grafana/detect-angular-dashboards/api/gcom"
Expand All @@ -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
Expand Down Expand Up @@ -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 = ""
Expand Down
70 changes: 65 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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)
}
}
}
32 changes: 31 additions & 1 deletion output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type Dashboard struct {

type Outputter interface {
Output([]Dashboard) error
BulkOutput(map[int][]Dashboard) error
}

type LoggerReadableOutput struct {
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
Expand Down