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  }