sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/jira/jira.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package jira
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	stdio "io"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"sync"
    29  
    30  	"github.com/andygrunwald/go-jira"
    31  	"github.com/hashicorp/go-retryablehttp"
    32  	"github.com/sirupsen/logrus"
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  
    35  	"sigs.k8s.io/prow/pkg/version"
    36  )
    37  
    38  // These are all the current valid states for Red Hat bugs in jira
    39  const (
    40  	StatusNew            = "NEW"
    41  	StatusBacklog        = "BACKLOG"
    42  	StatusAssigned       = "ASSIGNED"
    43  	StatusInProgess      = "IN PROGRESS"
    44  	StatusModified       = "MODIFIED"
    45  	StatusPost           = "POST"
    46  	StatusOnDev          = "ON_DEV"
    47  	StatusOnQA           = "ON_QA"
    48  	StatusVerified       = "VERIFIED"
    49  	StatusReleasePending = "RELEASE PENDING"
    50  	StatusClosed         = "CLOSED"
    51  )
    52  
    53  type Client interface {
    54  	GetIssue(id string) (*jira.Issue, error)
    55  	// SearchWithContext will search for tickets according to the jql
    56  	// Jira API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
    57  	SearchWithContext(ctx context.Context, jql string, options *jira.SearchOptions) ([]jira.Issue, *jira.Response, error)
    58  	UpdateIssue(*jira.Issue) (*jira.Issue, error)
    59  	CreateIssue(*jira.Issue) (*jira.Issue, error)
    60  	CreateIssueLink(*jira.IssueLink) error
    61  	// CloneIssue copies an issue struct, clears unsettable fields, creates a new
    62  	// issue using the updated struct, and then links the new issue as a clone to
    63  	// the original.
    64  	CloneIssue(*jira.Issue) (*jira.Issue, error)
    65  	GetTransitions(issueID string) ([]jira.Transition, error)
    66  	DoTransition(issueID, transitionID string) error
    67  	// UpdateStatus updates an issue's status by identifying the ID of the provided
    68  	// statusName and then doing the status transition to update the issue.
    69  	UpdateStatus(issueID, statusName string) error
    70  	// GetIssueSecurityLevel returns the security level of an issue. If no security level
    71  	// is set for the issue, the returned SecurityLevel and error will both be nil and
    72  	// the issue will follow the default project security level.
    73  	GetIssueSecurityLevel(*jira.Issue) (*SecurityLevel, error)
    74  	// GetIssueQaContact get the user details for the QA contact. The QA contact is a custom field in Jira
    75  	GetIssueQaContact(*jira.Issue) (*jira.User, error)
    76  	// GetIssueTargetVersion get the issue Target Release. The target release is a custom field in Jira
    77  	GetIssueTargetVersion(issue *jira.Issue) (*[]*jira.Version, error)
    78  	// FindUser returns all users with a field matching the queryParam (ex: email, display name, etc.)
    79  	FindUser(queryParam string) ([]*jira.User, error)
    80  	GetRemoteLinks(id string) ([]jira.RemoteLink, error)
    81  	AddRemoteLink(id string, link *jira.RemoteLink) (*jira.RemoteLink, error)
    82  	UpdateRemoteLink(id string, link *jira.RemoteLink) error
    83  	DeleteLink(id string) error
    84  	DeleteRemoteLink(issueID string, linkID int) error
    85  	// DeleteRemoteLinkViaURL identifies and removes a remote link from an issue
    86  	// the has the provided URL. The returned bool indicates whether a change
    87  	// was made during the operation as a remote link with the URL not existing
    88  	// is not consider an error for this function.
    89  	DeleteRemoteLinkViaURL(issueID, url string) (bool, error)
    90  	ForPlugin(plugin string) Client
    91  	AddComment(issueID string, comment *jira.Comment) (*jira.Comment, error)
    92  	ListProjects() (*jira.ProjectList, error)
    93  	JiraClient() *jira.Client
    94  	JiraURL() string
    95  	Used() bool
    96  	WithFields(fields logrus.Fields) Client
    97  	GetProjectVersions(project string) ([]*jira.Version, error)
    98  }
    99  
   100  type BasicAuthGenerator func() (username, password string)
   101  type BearerAuthGenerator func() (token string)
   102  
   103  type Options struct {
   104  	BasicAuth  BasicAuthGenerator
   105  	BearerAuth BearerAuthGenerator
   106  	LogFields  logrus.Fields
   107  }
   108  
   109  type Option func(*Options)
   110  
   111  func WithBasicAuth(basicAuth BasicAuthGenerator) Option {
   112  	return func(o *Options) {
   113  		o.BasicAuth = basicAuth
   114  	}
   115  }
   116  
   117  func WithBearerAuth(token BearerAuthGenerator) Option {
   118  	return func(o *Options) {
   119  		o.BearerAuth = token
   120  	}
   121  }
   122  
   123  func WithFields(fields logrus.Fields) Option {
   124  	return func(o *Options) {
   125  		o.LogFields = fields
   126  	}
   127  }
   128  
   129  func newJiraClient(endpoint string, o Options, retryingClient *retryablehttp.Client) (*jira.Client, error) {
   130  	retryingClient.HTTPClient.Transport = &metricsTransport{
   131  		upstream:       retryingClient.HTTPClient.Transport,
   132  		pathSimplifier: pathSimplifier().Simplify,
   133  		recorder:       requestResults,
   134  	}
   135  	retryingClient.HTTPClient.Transport = userAgentSettingTransport{
   136  		userAgent: version.UserAgent(),
   137  		upstream:  retryingClient.HTTPClient.Transport,
   138  	}
   139  
   140  	if o.BasicAuth != nil {
   141  		retryingClient.HTTPClient.Transport = &basicAuthRoundtripper{
   142  			generator: o.BasicAuth,
   143  			upstream:  retryingClient.HTTPClient.Transport,
   144  		}
   145  	}
   146  
   147  	if o.BearerAuth != nil {
   148  		retryingClient.HTTPClient.Transport = &bearerAuthRoundtripper{
   149  			generator: o.BearerAuth,
   150  			upstream:  retryingClient.HTTPClient.Transport,
   151  		}
   152  	}
   153  
   154  	return jira.NewClient(retryingClient.StandardClient(), endpoint)
   155  }
   156  
   157  func NewClient(endpoint string, opts ...Option) (Client, error) {
   158  	o := Options{}
   159  	for _, opt := range opts {
   160  		opt(&o)
   161  	}
   162  
   163  	log := logrus.WithField("client", "jira")
   164  	if len(o.LogFields) > 0 {
   165  		log = log.WithFields(o.LogFields)
   166  	}
   167  	retryingClient := retryablehttp.NewClient()
   168  	usedFlagTransport := &clientUsedTransport{
   169  		m:        sync.Mutex{},
   170  		upstream: retryingClient.HTTPClient.Transport,
   171  	}
   172  	retryingClient.HTTPClient.Transport = usedFlagTransport
   173  	retryingClient.Logger = &retryableHTTPLogrusWrapper{log: log}
   174  
   175  	jiraClient, err := newJiraClient(endpoint, o, retryingClient)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	url := jiraClient.GetBaseURL()
   180  	return &client{delegate: &delegate{url: url.String(), options: o}, logger: log, upstream: jiraClient, clientUsed: usedFlagTransport}, err
   181  }
   182  
   183  type userAgentSettingTransport struct {
   184  	userAgent string
   185  	upstream  http.RoundTripper
   186  }
   187  
   188  func (u userAgentSettingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
   189  	r.Header.Set("User-Agent", u.userAgent)
   190  	return u.upstream.RoundTrip(r)
   191  }
   192  
   193  type clientUsedTransport struct {
   194  	used     bool
   195  	m        sync.Mutex
   196  	upstream http.RoundTripper
   197  }
   198  
   199  func (c *clientUsedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
   200  	c.m.Lock()
   201  	c.used = true
   202  	c.m.Unlock()
   203  	return c.upstream.RoundTrip(r)
   204  }
   205  
   206  func (c *clientUsedTransport) Used() bool {
   207  	c.m.Lock()
   208  	defer c.m.Unlock()
   209  	return c.used
   210  }
   211  
   212  type used interface {
   213  	Used() bool
   214  }
   215  
   216  type client struct {
   217  	logger     *logrus.Entry
   218  	upstream   *jira.Client
   219  	clientUsed used
   220  	*delegate
   221  }
   222  
   223  // delegate actually does the work to talk to Jira
   224  type delegate struct {
   225  	url     string
   226  	options Options
   227  }
   228  
   229  func (jc *client) JiraClient() *jira.Client {
   230  	return jc.upstream
   231  }
   232  
   233  func (jc *client) GetIssue(id string) (*jira.Issue, error) {
   234  	issue, response, err := jc.upstream.Issue.Get(id, &jira.GetQueryOptions{})
   235  	if err != nil {
   236  		if response != nil && response.StatusCode == http.StatusNotFound {
   237  			return nil, NotFoundError{err}
   238  		}
   239  		return nil, HandleJiraError(response, err)
   240  	}
   241  
   242  	return issue, nil
   243  }
   244  
   245  func (jc *client) ListProjects() (*jira.ProjectList, error) {
   246  	projects, response, err := jc.upstream.Project.GetList()
   247  	if err != nil {
   248  		return nil, HandleJiraError(response, err)
   249  	}
   250  	return projects, nil
   251  }
   252  
   253  func IsNotFound(err error) bool {
   254  	return errors.Is(err, NotFoundError{})
   255  }
   256  
   257  func NewNotFoundError(err error) error {
   258  	return NotFoundError{err}
   259  }
   260  
   261  type NotFoundError struct {
   262  	error
   263  }
   264  
   265  func (NotFoundError) Is(target error) bool {
   266  	_, match := target.(NotFoundError)
   267  	return match
   268  }
   269  
   270  func (jc *client) GetRemoteLinks(id string) ([]jira.RemoteLink, error) {
   271  	result, resp, err := jc.upstream.Issue.GetRemoteLinks(id)
   272  	if err != nil {
   273  		return nil, HandleJiraError(resp, err)
   274  	}
   275  	return *result, nil
   276  }
   277  
   278  func (jc *client) AddRemoteLink(id string, link *jira.RemoteLink) (*jira.RemoteLink, error) {
   279  	result, resp, err := jc.upstream.Issue.AddRemoteLink(id, link)
   280  	if err != nil {
   281  		return nil, fmt.Errorf("failed to add link: %w", HandleJiraError(resp, err))
   282  	}
   283  	return result, nil
   284  }
   285  
   286  func (jc *client) DeleteLink(linkID string) error {
   287  	resp, err := jc.upstream.Issue.DeleteLink(linkID)
   288  	if err != nil {
   289  		return HandleJiraError(resp, err)
   290  	}
   291  	return nil
   292  }
   293  
   294  func (jc *client) UpdateRemoteLink(id string, link *jira.RemoteLink) error {
   295  	internalLinkId := fmt.Sprint(link.ID)
   296  	req, err := jc.upstream.NewRequest("PUT", "rest/api/2/issue/"+id+"/remotelink/"+internalLinkId, link)
   297  	if err != nil {
   298  		return fmt.Errorf("failed to construct request: %w", err)
   299  	}
   300  	resp, err := jc.upstream.Do(req, nil)
   301  	if resp != nil {
   302  		defer resp.Body.Close()
   303  	}
   304  	if err != nil {
   305  		return fmt.Errorf("failed to update link: %w", HandleJiraError(resp, err))
   306  	}
   307  	if resp.StatusCode != http.StatusNoContent {
   308  		return fmt.Errorf("failed to update link: expected status code %d but got %d instead", http.StatusNoContent, resp.StatusCode)
   309  	}
   310  	return nil
   311  }
   312  
   313  func (jc *client) DeleteRemoteLink(issueID string, linkID int) error {
   314  	apiEndpoint := fmt.Sprintf("/rest/api/2/issue/%s/remotelink/%d", issueID, linkID)
   315  	req, err := jc.upstream.NewRequest("DELETE", apiEndpoint, nil)
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	// the response should be empty if it is not an error
   321  	resp, err := jc.upstream.Do(req, nil)
   322  	if resp != nil {
   323  		defer resp.Body.Close()
   324  	}
   325  	// Status code 204 is a success for this function. On success, there will be an error message of `EOF`,
   326  	// so in addition to the nil check for the error, we must check the status code.
   327  	if resp.StatusCode != 204 && err != nil {
   328  		return HandleJiraError(resp, err)
   329  	}
   330  	return nil
   331  }
   332  
   333  // DeleteRemoteLinkViaURL identifies and removes a remote link from an issue
   334  // the has the provided URL. The returned bool indicates whether a change
   335  // was made during the operation as a remote link with the URL not existing
   336  // is not consider an error for this function.
   337  func DeleteRemoteLinkViaURL(jc Client, issueID, url string) (bool, error) {
   338  	links, err := jc.GetRemoteLinks(issueID)
   339  	if err != nil {
   340  		return false, err
   341  	}
   342  	for _, link := range links {
   343  		if link.Object.URL == url {
   344  			return true, jc.DeleteRemoteLink(issueID, link.ID)
   345  		}
   346  	}
   347  	return false, fmt.Errorf("could not find remote link on issue with URL `%s`", url)
   348  }
   349  
   350  func (jc *client) DeleteRemoteLinkViaURL(issueID, url string) (bool, error) {
   351  	return DeleteRemoteLinkViaURL(jc, issueID, url)
   352  }
   353  
   354  func (jc *client) FindUser(queryParam string) ([]*jira.User, error) {
   355  	// JIRA's own documentation here is incorrect; it specifies that either 'accountID',
   356  	// 'query', or 'property' must be used. However, JIRA throws an error unless 'username'
   357  	// is used. This does a search as if it were supposed to be the query param, so we can use it like that
   358  	queryString := "username='" + queryParam + "'"
   359  	queryString = url.PathEscape(queryString)
   360  
   361  	apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString)
   362  	req, err := jc.upstream.NewRequest("GET", apiEndpoint, nil)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  
   367  	users := []*jira.User{}
   368  	resp, err := jc.upstream.Do(req, &users)
   369  	if resp != nil {
   370  		defer resp.Body.Close()
   371  	}
   372  	if err != nil {
   373  		return nil, HandleJiraError(resp, err)
   374  	}
   375  	return users, nil
   376  }
   377  
   378  // UpdateStatus updates an issue's status by identifying the ID of the provided
   379  // statusName and then doing the status transition using the provided client to update the issue.
   380  func UpdateStatus(jc Client, issueID, statusName string) error {
   381  	transitions, err := jc.GetTransitions(issueID)
   382  	if err != nil {
   383  		return err
   384  	}
   385  	transitionID := ""
   386  	var nameList []string
   387  	for _, transition := range transitions {
   388  		// JIRA shows all statuses as caps in the UI, but internally has different case; use EqualFold to ignore case
   389  		if strings.EqualFold(transition.Name, statusName) {
   390  			transitionID = transition.ID
   391  			break
   392  		}
   393  		nameList = append(nameList, transition.Name)
   394  	}
   395  	if transitionID == "" {
   396  		return fmt.Errorf("No transition status with name `%s` could be found. Please select from the following list: %v", statusName, nameList)
   397  	}
   398  	return jc.DoTransition(issueID, transitionID)
   399  }
   400  
   401  func (jc *client) UpdateStatus(issueID, statusName string) error {
   402  	return UpdateStatus(jc, issueID, statusName)
   403  }
   404  
   405  func (jc *client) GetTransitions(issueID string) ([]jira.Transition, error) {
   406  	transitions, resp, err := jc.upstream.Issue.GetTransitions(issueID)
   407  	if err != nil {
   408  		return nil, HandleJiraError(resp, err)
   409  	}
   410  	return transitions, nil
   411  }
   412  
   413  func (jc *client) DoTransition(issueID, transitionID string) error {
   414  	resp, err := jc.upstream.Issue.DoTransition(issueID, transitionID)
   415  	if err != nil {
   416  		return HandleJiraError(resp, err)
   417  	}
   418  	return nil
   419  }
   420  
   421  func (jc *client) UpdateIssue(issue *jira.Issue) (*jira.Issue, error) {
   422  	result, resp, err := jc.upstream.Issue.Update(issue)
   423  	if err != nil {
   424  		return nil, HandleJiraError(resp, err)
   425  	}
   426  	return result, nil
   427  }
   428  
   429  func (jc *client) AddComment(issueID string, comment *jira.Comment) (*jira.Comment, error) {
   430  	result, resp, err := jc.upstream.Issue.AddComment(issueID, comment)
   431  	if err != nil {
   432  		return nil, HandleJiraError(resp, err)
   433  	}
   434  	return result, nil
   435  }
   436  
   437  func (jc *client) CreateIssue(issue *jira.Issue) (*jira.Issue, error) {
   438  	result, resp, err := jc.upstream.Issue.Create(issue)
   439  	if err != nil {
   440  		return nil, HandleJiraError(resp, err)
   441  	}
   442  	return result, nil
   443  }
   444  
   445  func (jc *client) CreateIssueLink(link *jira.IssueLink) error {
   446  	resp, err := jc.upstream.Issue.AddLink(link)
   447  	if err != nil {
   448  		return HandleJiraError(resp, err)
   449  	}
   450  	return nil
   451  }
   452  
   453  // CloneIssue copies an issue struct, clears unsettable fields, creates a new
   454  // issue using the updated struct, and then links the new issue as a clone to
   455  // the original.
   456  func CloneIssue(jc Client, parent *jira.Issue) (*jira.Issue, error) {
   457  	// create deep copy of parent "Fields" field
   458  	data, err := json.Marshal(parent.Fields)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  	childIssueFields := &jira.IssueFields{}
   463  	err = json.Unmarshal(data, childIssueFields)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  	childIssue := &jira.Issue{
   468  		Fields: childIssueFields,
   469  	}
   470  	// update description
   471  	childIssue.Fields.Description = fmt.Sprintf("This is a clone of issue %s. The following is the description of the original issue: \n---\n%s", parent.Key, parent.Fields.Description)
   472  
   473  	// attempt to create the new issue
   474  	createdIssue, err := jc.CreateIssue(childIssue)
   475  	if err != nil {
   476  		// some fields cannot be set on creation; unset them
   477  		if JiraErrorStatusCode(err) != 400 {
   478  			return nil, err
   479  		}
   480  		var newErr error
   481  		childIssue, newErr = unsetProblematicFields(childIssue, JiraErrorBody(err))
   482  		if newErr != nil {
   483  			// in this situation, it makes more sense to just return the original error; any error from unsetProblematicFields will be
   484  			// a json marshalling error, indicating an error different from the standard non-settable fields error. The error from
   485  			// unsetProblematicFields is not useful in these cases
   486  			return nil, err
   487  		}
   488  		createdIssue, err = jc.CreateIssue(childIssue)
   489  		if err != nil {
   490  			return nil, err
   491  		}
   492  	}
   493  
   494  	// create clone links
   495  	link := &jira.IssueLink{
   496  		OutwardIssue: &jira.Issue{ID: parent.ID},
   497  		InwardIssue:  &jira.Issue{ID: createdIssue.ID},
   498  		Type: jira.IssueLinkType{
   499  			Name:    "Cloners",
   500  			Inward:  "is cloned by",
   501  			Outward: "clones",
   502  		},
   503  	}
   504  	if err := jc.CreateIssueLink(link); err != nil {
   505  		return nil, err
   506  	}
   507  	// Get updated issue, which would have issue links
   508  	if clonedIssue, err := jc.GetIssue(createdIssue.ID); err != nil {
   509  		// still return the originally created child issue here in case of failure to get updated issue
   510  		return createdIssue, fmt.Errorf("Could not get issue after creating issue links: %w", err)
   511  	} else {
   512  		return clonedIssue, nil
   513  	}
   514  }
   515  
   516  func (jc *client) CloneIssue(parent *jira.Issue) (*jira.Issue, error) {
   517  	return CloneIssue(jc, parent)
   518  }
   519  
   520  func unsetProblematicFields(issue *jira.Issue, responseBody string) (*jira.Issue, error) {
   521  	// handle unsettable "unknown" fields
   522  	processedResponse := CreateIssueError{}
   523  	if newErr := json.Unmarshal([]byte(responseBody), &processedResponse); newErr != nil {
   524  		return nil, fmt.Errorf("Error processing jira error: %w", newErr)
   525  	}
   526  	// turn issue into map to simplify unsetting process
   527  	marshalledIssue, err := json.Marshal(issue)
   528  	if err != nil {
   529  		return nil, err
   530  	}
   531  	issueMap := make(map[string]interface{})
   532  	if err := json.Unmarshal(marshalledIssue, &issueMap); err != nil {
   533  		return nil, err
   534  	}
   535  	fieldsMap := issueMap["fields"].(map[string]interface{})
   536  	for field := range processedResponse.Errors {
   537  		delete(fieldsMap, field)
   538  	}
   539  	// Remove null value "customfields_" because they cause the server to return: 500 Internal Server Error
   540  	for field, value := range fieldsMap {
   541  		if strings.HasPrefix(field, "customfield_") && value == nil {
   542  			delete(fieldsMap, field)
   543  		}
   544  	}
   545  	issueMap["fields"] = fieldsMap
   546  	// turn back into jira.Issue type
   547  	marshalledFixedIssue, err := json.Marshal(issueMap)
   548  	if err != nil {
   549  		return nil, err
   550  	}
   551  	newIssue := jira.Issue{}
   552  	if err := json.Unmarshal(marshalledFixedIssue, &newIssue); err != nil {
   553  		return nil, err
   554  	}
   555  	return &newIssue, nil
   556  }
   557  
   558  type CreateIssueError struct {
   559  	ErrorMessages []string          `json:"errorMessages"`
   560  	Errors        map[string]string `json:"errors"`
   561  }
   562  
   563  type SecurityLevel struct {
   564  	Self        string `json:"self"`
   565  	ID          string `json:"id"`
   566  	Name        string `json:"name"`
   567  	Description string `json:"description"`
   568  }
   569  
   570  // Used determines whether the client has been used
   571  func (jc *client) Used() bool {
   572  	return jc.clientUsed.Used()
   573  }
   574  
   575  type bearerAuthRoundtripper struct {
   576  	generator BearerAuthGenerator
   577  	upstream  http.RoundTripper
   578  }
   579  
   580  // WithFields clones the client, keeping the underlying delegate the same but adding
   581  // fields to the logging context
   582  func (jc *client) WithFields(fields logrus.Fields) Client {
   583  	return &client{
   584  		clientUsed: jc.clientUsed,
   585  		upstream:   jc.upstream,
   586  		logger:     jc.logger.WithFields(fields),
   587  		delegate:   jc.delegate,
   588  	}
   589  }
   590  
   591  // ForPlugin clones the client, keeping the underlying delegate the same but adding
   592  // a plugin identifier and log field
   593  func (jc *client) ForPlugin(plugin string) Client {
   594  	pluginLogger := jc.logger.WithField("plugin", plugin)
   595  	retryingClient := retryablehttp.NewClient()
   596  	usedFlagTransport := &clientUsedTransport{
   597  		m:        sync.Mutex{},
   598  		upstream: retryingClient.HTTPClient.Transport,
   599  	}
   600  	retryingClient.HTTPClient.Transport = usedFlagTransport
   601  	retryingClient.Logger = &retryableHTTPLogrusWrapper{log: pluginLogger}
   602  	// ignore error as url.String() was passed to the delegate
   603  	jiraClient, err := newJiraClient(jc.url, jc.options, retryingClient)
   604  	if err != nil {
   605  		pluginLogger.WithError(err).Error("invalid Jira URL")
   606  		jiraClient = jc.upstream
   607  	}
   608  	return &client{
   609  		logger:     pluginLogger,
   610  		clientUsed: usedFlagTransport,
   611  		upstream:   jiraClient,
   612  		delegate:   jc.delegate,
   613  	}
   614  }
   615  
   616  func (jc *client) JiraURL() string {
   617  	return jc.url
   618  }
   619  
   620  func (bart *bearerAuthRoundtripper) RoundTrip(req *http.Request) (*http.Response, error) {
   621  	req2 := new(http.Request)
   622  	*req2 = *req
   623  	req2.URL = new(url.URL)
   624  	*req2.URL = *req.URL
   625  	token := bart.generator()
   626  	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
   627  	logrus.WithField("curl", toCurl(req2)).Trace("Executing http request")
   628  	return bart.upstream.RoundTrip(req2)
   629  }
   630  
   631  type basicAuthRoundtripper struct {
   632  	generator BasicAuthGenerator
   633  	upstream  http.RoundTripper
   634  }
   635  
   636  func (bart *basicAuthRoundtripper) RoundTrip(req *http.Request) (*http.Response, error) {
   637  	req2 := new(http.Request)
   638  	*req2 = *req
   639  	req2.URL = new(url.URL)
   640  	*req2.URL = *req.URL
   641  	user, pass := bart.generator()
   642  	req2.SetBasicAuth(user, pass)
   643  	logrus.WithField("curl", toCurl(req2)).Trace("Executing http request")
   644  	return bart.upstream.RoundTrip(req2)
   645  }
   646  
   647  var knownAuthTypes = sets.New[string]("bearer", "basic", "negotiate")
   648  
   649  // maskAuthorizationHeader masks credential content from authorization headers
   650  // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
   651  func maskAuthorizationHeader(key string, value string) string {
   652  	if !strings.EqualFold(key, "Authorization") {
   653  		return value
   654  	}
   655  	if len(value) == 0 {
   656  		return ""
   657  	}
   658  	var authType string
   659  	if i := strings.Index(value, " "); i > 0 {
   660  		authType = value[0:i]
   661  	} else {
   662  		authType = value
   663  	}
   664  	if !knownAuthTypes.Has(strings.ToLower(authType)) {
   665  		return "<masked>"
   666  	}
   667  	if len(value) > len(authType)+1 {
   668  		value = authType + " <masked>"
   669  	} else {
   670  		value = authType
   671  	}
   672  	return value
   673  }
   674  
   675  type JiraError struct {
   676  	StatusCode    int
   677  	Body          string
   678  	OriginalError error
   679  }
   680  
   681  func (e JiraError) Error() string {
   682  	return fmt.Sprintf("%s: %s", e.OriginalError, e.Body)
   683  }
   684  
   685  // JiraErrorStatusCode will identify if an error is a JiraError and return the
   686  // stored status code if it is; if it is not, `-1` will be returned
   687  func JiraErrorStatusCode(err error) int {
   688  	if jiraErr := (&JiraError{}); errors.As(err, &jiraErr) {
   689  		return jiraErr.StatusCode
   690  	}
   691  	jiraErr, ok := err.(*JiraError)
   692  	if !ok {
   693  		return -1
   694  	}
   695  	return jiraErr.StatusCode
   696  }
   697  
   698  // JiraErrorBody will identify if an error is a JiraError and return the stored
   699  // response body if it is; if it is not, an empty string will be returned
   700  func JiraErrorBody(err error) string {
   701  	if jiraErr := (&JiraError{}); errors.As(err, &jiraErr) {
   702  		return jiraErr.Body
   703  	}
   704  	jiraErr, ok := err.(*JiraError)
   705  	if !ok {
   706  		return ""
   707  	}
   708  	return jiraErr.Body
   709  }
   710  
   711  // HandleJiraError collapses cryptic Jira errors to include response
   712  // bodies if it's detected that the original error holds no
   713  // useful context in the first place
   714  func HandleJiraError(response *jira.Response, err error) error {
   715  	if err != nil && strings.Contains(err.Error(), "Please analyze the request body for more details.") {
   716  		if response != nil && response.Response != nil {
   717  			body, readError := stdio.ReadAll(response.Body)
   718  			if readError != nil && readError.Error() != "http: read on closed response body" {
   719  				logrus.WithError(readError).Warn("Failed to read Jira response body.")
   720  			}
   721  			return &JiraError{
   722  				StatusCode:    response.StatusCode,
   723  				Body:          string(body),
   724  				OriginalError: err,
   725  			}
   726  		}
   727  	}
   728  	return err
   729  }
   730  
   731  // toCurl is a slightly adjusted copy of https://github.com/kubernetes/kubernetes/blob/74053d555d71a14e3853b97e204d7d6415521375/staging/src/k8s.io/client-go/transport/round_trippers.go#L339
   732  func toCurl(r *http.Request) string {
   733  	headers := ""
   734  	for key, values := range r.Header {
   735  		for _, value := range values {
   736  			headers += fmt.Sprintf(` -H %q`, fmt.Sprintf("%s: %s", key, maskAuthorizationHeader(key, value)))
   737  		}
   738  	}
   739  
   740  	return fmt.Sprintf("curl -k -v -X%s %s '%s'", r.Method, headers, r.URL.String())
   741  }
   742  
   743  type retryableHTTPLogrusWrapper struct {
   744  	log *logrus.Entry
   745  }
   746  
   747  // fieldsForContext translates a list of context fields to a
   748  // logrus format; any items that don't conform to our expectations
   749  // are omitted
   750  func (l *retryableHTTPLogrusWrapper) fieldsForContext(context ...interface{}) logrus.Fields {
   751  	fields := logrus.Fields{}
   752  	for i := 0; i < len(context)-1; i += 2 {
   753  		key, ok := context[i].(string)
   754  		if !ok {
   755  			continue
   756  		}
   757  		fields[key] = context[i+1]
   758  	}
   759  	return fields
   760  }
   761  
   762  func (l *retryableHTTPLogrusWrapper) Error(msg string, context ...interface{}) {
   763  	l.log.WithFields(l.fieldsForContext(context...)).Error(msg)
   764  }
   765  
   766  func (l *retryableHTTPLogrusWrapper) Info(msg string, context ...interface{}) {
   767  	l.log.WithFields(l.fieldsForContext(context...)).Info(msg)
   768  }
   769  
   770  func (l *retryableHTTPLogrusWrapper) Debug(msg string, context ...interface{}) {
   771  	l.log.WithFields(l.fieldsForContext(context...)).Debug(msg)
   772  }
   773  
   774  func (l *retryableHTTPLogrusWrapper) Warn(msg string, context ...interface{}) {
   775  	l.log.WithFields(l.fieldsForContext(context...)).Warn(msg)
   776  }
   777  
   778  func (jc *client) SearchWithContext(ctx context.Context, jql string, options *jira.SearchOptions) ([]jira.Issue, *jira.Response, error) {
   779  	issues, response, err := jc.upstream.Issue.SearchWithContext(ctx, jql, options)
   780  	if err != nil {
   781  		if response != nil && response.StatusCode == http.StatusNotFound {
   782  			return nil, response, NotFoundError{err}
   783  		}
   784  		return nil, response, HandleJiraError(response, err)
   785  	}
   786  	return issues, response, nil
   787  }
   788  
   789  func GetUnknownField(field string, issue *jira.Issue, fn func() interface{}) error {
   790  	obj := fn()
   791  	unknownField, ok := issue.Fields.Unknowns[field]
   792  	if !ok {
   793  		return nil
   794  	}
   795  	bytes, err := json.Marshal(unknownField)
   796  	if err != nil {
   797  		return fmt.Errorf("failed to process the custom field %s. Error : %v", field, err)
   798  	}
   799  	if err := json.Unmarshal(bytes, obj); err != nil {
   800  		return fmt.Errorf("failed to unmarshall the json to struct for %s. Error: %v", field, err)
   801  	}
   802  	return err
   803  
   804  }
   805  
   806  // GetIssueSecurityLevel returns the security level of an issue. If no security level
   807  // is set for the issue, the returned SecurityLevel and error will both be nil and
   808  // the issue will follow the default project security level.
   809  func GetIssueSecurityLevel(issue *jira.Issue) (*SecurityLevel, error) {
   810  	// TODO: Add field to the upstream go-jira package; if a security level exists, it is returned
   811  	// as part of the issue fields
   812  	// See https://github.com/andygrunwald/go-jira/issues/456
   813  	var obj *SecurityLevel
   814  	err := GetUnknownField("security", issue, func() interface{} {
   815  		obj = &SecurityLevel{}
   816  		return obj
   817  	})
   818  	return obj, err
   819  }
   820  
   821  func (jc *client) GetIssueSecurityLevel(issue *jira.Issue) (*SecurityLevel, error) {
   822  	return GetIssueSecurityLevel(issue)
   823  }
   824  
   825  func GetIssueQaContact(issue *jira.Issue) (*jira.User, error) {
   826  	var obj *jira.User
   827  	err := GetUnknownField("customfield_12316243", issue, func() interface{} {
   828  		obj = &jira.User{}
   829  		return obj
   830  	})
   831  	return obj, err
   832  }
   833  
   834  func (jc *client) GetIssueQaContact(issue *jira.Issue) (*jira.User, error) {
   835  	return GetIssueQaContact(issue)
   836  }
   837  
   838  func GetIssueTargetVersion(issue *jira.Issue) (*[]*jira.Version, error) {
   839  	var obj *[]*jira.Version
   840  	err := GetUnknownField("customfield_12319940", issue, func() interface{} {
   841  		obj = &[]*jira.Version{{}}
   842  		return obj
   843  	})
   844  	return obj, err
   845  }
   846  
   847  func (jc *client) GetIssueTargetVersion(issue *jira.Issue) (*[]*jira.Version, error) {
   848  	return GetIssueTargetVersion(issue)
   849  }
   850  
   851  // GetProjectVersions returns the list of all the Versions defined in a Project
   852  func (jc *client) GetProjectVersions(project string) ([]*jira.Version, error) {
   853  	req, err := jc.upstream.NewRequest("GET", "rest/api/2/project/"+project+"/versions", nil)
   854  	if err != nil {
   855  		return nil, fmt.Errorf("failed to construct request: %w", err)
   856  	}
   857  	versions := []*jira.Version{}
   858  	resp, err := jc.upstream.Do(req, &versions)
   859  	if resp != nil {
   860  		defer resp.Body.Close()
   861  	}
   862  	if err != nil {
   863  		return nil, HandleJiraError(resp, err)
   864  	}
   865  	return versions, nil
   866  }