go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/issuetracker/issuetracker.go (about) 1 // Copyright 2023 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package issuetracker 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "log" 13 "net/http" 14 15 "github.com/pkg/errors" 16 "go.skia.org/infra/go/httputils" 17 "go.skia.org/infra/go/luciauth" 18 "go.skia.org/infra/go/util" 19 ) 20 21 const ( 22 issuesURL = "https://issuetracker.googleapis.com/v1/issues" 23 scope = "https://www.googleapis.com/auth/buganizer" 24 ) 25 26 type IssueTracker struct { 27 client *http.Client 28 url string 29 } 30 31 // Issue represents the body of the request/response for creating new issues. 32 // The full schema can be found at https://go/issuetracker.proto. 33 type Issue struct { 34 IssueID string `json:"issueId,omitempty"` 35 IssueState IssueState `json:"issueState"` 36 IssueComment IssueComment `json:"issueComment"` 37 } 38 39 // IssueState represents the state of the issue. 40 type IssueState struct { 41 ComponentID string `json:"componentId"` 42 Type string `json:"type"` 43 Status string `json:"status"` 44 Priority string `json:"priority"` 45 Severity string `json:"severity"` 46 Title string `json:"title"` 47 AccessLimit AccessLimit `json:"accessLimit,omitempty"` 48 } 49 50 type AccessLimit struct { 51 AccessLevel string `json:"accessLevel"` 52 } 53 54 type IssueComment struct { 55 Comment string `json:"comment"` 56 FormattingMode string `json:"formattingMode,omitempty"` 57 } 58 59 // NewIssueTracker creates and returns an IssueTracker instance. 60 func NewIssueTracker(client *http.Client) *IssueTracker { 61 return &IssueTracker{ 62 client: client, 63 url: issuesURL, 64 } 65 } 66 67 func NewIssueTrackerFromLUCIContext() (*IssueTracker, error) { 68 ts, err := luciauth.NewLUCIContextTokenSource(scope) 69 if err != nil { 70 return nil, err 71 } 72 client := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client() 73 return NewIssueTracker(client), nil 74 } 75 76 // AddIssue creates an issue with the passed in params. 77 func (m *IssueTracker) AddIssue(issue Issue) (Issue, error) { 78 newIssueResponse := Issue{} 79 80 callback := func(resp []byte) error { 81 if err := json.Unmarshal(resp, &newIssueResponse); err != nil { 82 return errors.Wrap(err, "unable to unmarshal new issue response") 83 } 84 issueURL := fmt.Sprintf("https://issuetracker.google.com/%s", newIssueResponse.IssueID) 85 log.Printf("Issue filed!\nIssue:\n%v\nReference: %s\n", newIssueResponse, issueURL) 86 return nil 87 } 88 err := post(m.client, m.url, issue, callback) 89 return newIssueResponse, err 90 } 91 92 func post(client *http.Client, dst string, request any, callback func(r []byte) error) error { 93 b := &bytes.Buffer{} 94 e := json.NewEncoder(b) 95 if err := e.Encode(request); err != nil { 96 return errors.Wrap(err, "failed to encode json for request") 97 } 98 99 resp, err := client.Post(dst, "application/json", b) 100 if err != nil || resp == nil { 101 return errors.Wrap(err, "failed to retrieve IssueTracker response") 102 } 103 if resp.StatusCode != http.StatusOK { 104 return fmt.Errorf("Unexpected IssueTracker response. Expected %d, received %d", http.StatusOK, resp.StatusCode) 105 } 106 107 defer util.Close(resp.Body) 108 body, err := io.ReadAll(resp.Body) 109 if err != nil { 110 return errors.Wrap(err, "failed to read response body") 111 } 112 113 if err := callback(body); err != nil { 114 return errors.Wrap(err, "failed to log IssueTracker post request") 115 } 116 117 return nil 118 }