blob: c76e529cfb33589c40a549bc29d857541e9dc9a8 [file] [log] [blame]
package issuetracker
// Accesses issuetracker results from Google storage.
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"cloud.google.com/go/storage"
"go.skia.org/infra/bugs-central/go/bugs"
"go.skia.org/infra/bugs-central/go/types"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
var (
issueTrackerBucket = "skia-issuetracker-details"
// The file that contains issuetracker search results in the above bucket.
resultsFileName = "results.json"
)
type issueTrackerIssue struct {
Id int64 `json:"id"`
Status string `json:"status"`
Priority string `json:"priority"`
Assignee string `json:"assignee"`
CreatedTS int64 `json:"created_ts"`
ModifiedTS int64 `json:"modified_ts"`
}
// issueTracker implements bugs.BugsFramework for github repos.
type issueTracker struct {
storageClient *storage.Client
openIssues *bugs.OpenIssues
queryConfig *IssueTrackerQueryConfig
}
// New returns an instance of the issuetracker implementation of bugs.BugFramework.
func New(storageClient *storage.Client, openIssues *bugs.OpenIssues, queryConfig *IssueTrackerQueryConfig) (bugs.BugFramework, error) {
return &issueTracker{
storageClient: storageClient,
openIssues: openIssues,
queryConfig: queryConfig,
}, nil
}
// IssueTrackerQueryConfig is the config that will be used when querying issuetracker.
type IssueTrackerQueryConfig struct {
// Key to find the open bugs from the storage results file.
Query string
// Which client's issues we are looking for.
Client types.RecognizedClient
// Issues are considered untriaged if they have any of these priorities.
UntriagedPriorities []string
// Issues are also considered untriaged if they are assigned to any of these emails.
UntriagedAliases []string
}
// See documentation for bugs.Search interface.
func (it *issueTracker) Search(ctx context.Context) ([]*types.Issue, *types.IssueCountsData, error) {
obj := it.storageClient.Bucket(issueTrackerBucket).Object(resultsFileName)
reader, err := obj.NewReader(ctx)
if err != nil {
return nil, nil, skerr.Wrapf(err, "accessing gs://%s/%s failed", issueTrackerBucket, resultsFileName)
}
defer util.Close(reader)
var results map[string][]issueTrackerIssue
if err := json.NewDecoder(reader).Decode(&results); err != nil {
return nil, nil, skerr.Wrapf(err, "invalid JSON from %s", resultsFileName)
}
if _, ok := results[it.queryConfig.Query]; !ok {
return nil, nil, skerr.Fmt("could not find %s in %s", it.queryConfig.Query, resultsFileName)
}
// These are all the issues returned by the issuetracker query.
trackerIssues := results[it.queryConfig.Query]
// Convert issuetracker issues into bug_framework's generic issues
issues := []*types.Issue{}
countsData := &types.IssueCountsData{}
for _, i := range trackerIssues {
// Populate counts data.
countsData.OpenCount++
if i.Assignee == "" {
countsData.UnassignedCount++
}
created := time.Unix(i.CreatedTS, 0)
modified := time.Unix(i.ModifiedTS, 0)
priority := types.StandardizedPriority(i.Priority)
countsData.IncPriority(priority)
sloViolation, reason, d := types.IsPrioritySLOViolation(time.Now(), created, modified, priority)
countsData.IncSLOViolation(sloViolation, priority)
if util.In(i.Priority, it.queryConfig.UntriagedPriorities) {
countsData.UntriagedCount++
} else if util.In(i.Assignee, it.queryConfig.UntriagedAliases) {
countsData.UntriagedCount++
}
id := strconv.FormatInt(i.Id, 10)
issues = append(issues, &types.Issue{
Id: id,
State: i.Status,
Priority: priority,
Owner: i.Assignee,
CreatedTime: created,
ModifiedTime: modified,
SLOViolation: sloViolation,
SLOViolationReason: reason,
SLOViolationDuration: d,
Link: it.GetIssueLink("", id),
})
}
return issues, countsData, nil
}
// See documentation for bugs.SearchClientAndPersist interface.
func (it *issueTracker) SearchClientAndPersist(ctx context.Context, dbClient types.BugsDB, runId string) error {
qc := it.queryConfig
issues, countsData, err := it.Search(ctx)
if err != nil {
return skerr.Wrapf(err, "error when searching issuetracker")
}
sklog.Infof("%s issuetracker issues %+v", qc.Client, countsData)
queryDesc := qc.Query
countsData.QueryLink = fmt.Sprintf("http://b/issues?q=%s", qc.Query)
// Construct query for untriaged issues.
if len(qc.UntriagedPriorities) > 0 || len(qc.UntriagedAliases) > 0 {
untriagedPrioritiesTokens := []string{}
for _, p := range qc.UntriagedPriorities {
untriagedPrioritiesTokens = append(untriagedPrioritiesTokens, fmt.Sprintf("p:%s", p))
}
untriagedAliasesTokens := []string{}
for _, a := range qc.UntriagedAliases {
untriagedAliasesTokens = append(untriagedAliasesTokens, fmt.Sprintf("assignee:%s", a))
}
untriagedTokens := append(untriagedPrioritiesTokens, untriagedAliasesTokens...)
countsData.UntriagedQueryLink = fmt.Sprintf("%s (%s)", countsData.QueryLink, strings.Join(untriagedTokens, "|"))
// Calculate priority links.
countsData.P0Link = fmt.Sprintf("%s P:P0", countsData.QueryLink)
countsData.P1Link = fmt.Sprintf("%s P:P1", countsData.QueryLink)
countsData.P2Link = fmt.Sprintf("%s P:P2", countsData.QueryLink)
countsData.P3AndRestLink = fmt.Sprintf("%s (P:P3 P:P4)", countsData.QueryLink)
}
client := qc.Client
// Put in DB.
if err := dbClient.PutInDB(ctx, client, types.IssueTrackerSource, queryDesc, runId, countsData); err != nil {
return skerr.Wrapf(err, "error putting issuetracker results in DB")
}
// Put in memory.
it.openIssues.PutOpenIssues(client, types.IssueTrackerSource, queryDesc, issues)
return nil
}
// See documentation for bugs.GetIssueLink interface.
func (it *issueTracker) GetIssueLink(_, id string) string {
return fmt.Sprintf("http://b/%s", id)
}
// See documentation for bugs.SetOwnerAndAddComment interface.
func (it *issueTracker) SetOwnerAndAddComment(owner, comment, id string) error {
return errors.New("SetOwnerAndAddComment not implemented for issuetracker")
}