github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/thirdparty/jira.go (about)

     1  package thirdparty
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"math"
     8  	"net/url"
     9  
    10  	"github.com/pkg/errors"
    11  )
    12  
    13  // JiraTickets marshal to and unmarshal from the json issue
    14  // returned by the rest api at /rest/api/latest/issue/{ticket_id}
    15  type JiraTicket struct {
    16  	Key    string        `json:"key"`
    17  	Expand string        `json:"expand"`
    18  	Fields *TicketFields `json:"fields"`
    19  }
    20  
    21  // JiraSearchResults marshal to and unmarshal from the json
    22  // search results returned by the rest api at /rest/api/2/search?jql={jql}
    23  type JiraSearchResults struct {
    24  	Expand     string       `json:"expand"`
    25  	StartAt    int          `json:"startAt"`
    26  	MaxResults int          `json:"maxResults"`
    27  	Total      int          `json:"total"`
    28  	Issues     []JiraTicket `json:"issues"`
    29  }
    30  
    31  type TicketFields struct {
    32  	IssueType   *TicketType     `json:"issuetype"`
    33  	Summary     string          `json:"summary"`
    34  	Description string          `json:"description"`
    35  	Reporter    *User           `json:"reporter"`
    36  	Assignee    *User           `json:"assignee"`
    37  	Project     *JiraProject    `json:"project"`
    38  	Resolution  *JiraResolution `json:"resolution"`
    39  	Created     string          `json:"created"`
    40  	Updated     string          `json:"updated"`
    41  	Status      *JiraStatus     `json:"status"`
    42  }
    43  
    44  // JiraCreateTicketResponse contains the results of a JIRA create ticket API call.
    45  type JiraCreateTicketResponse struct {
    46  	Id   string `json:"id"`
    47  	Key  string `json:"key"`
    48  	Self string `json:"self"`
    49  }
    50  
    51  type JiraStatus struct {
    52  	Id   string `json:"id"`
    53  	Self string `json:"self"`
    54  	Name string `json:"name"`
    55  }
    56  
    57  type JiraResolution struct {
    58  	Id          string `json:"id"`
    59  	Self        string `json:"self"`
    60  	Name        string `json:"name"`
    61  	Description string `json:"description"`
    62  }
    63  
    64  type TicketType struct {
    65  	Id          string `json:"id"`
    66  	Self        string `json:"self"`
    67  	Description string `json:"description"`
    68  	IconUrl     string `json:"iconUrl"`
    69  	Name        string `json:"name"`
    70  	Subtask     bool   `json:"subtask"`
    71  }
    72  
    73  type JiraProject struct {
    74  	Id         string            `json:"id"`
    75  	Self       string            `json:"self"`
    76  	Key        string            `json:"key"`
    77  	Name       string            `json:"name"`
    78  	AvatarUrls map[string]string `json:"avatarUrls"`
    79  }
    80  
    81  type User struct {
    82  	Id           string            `json:"id"`
    83  	Self         string            `json:"self"`
    84  	Name         string            `json:"name"`
    85  	EmailAddress string            `json:"emailAddress"`
    86  	DisplayName  string            `json:"displayName"`
    87  	Active       bool              `json:"active"`
    88  	TimeZone     string            `json:"timeZone"`
    89  	AvatarUrls   map[string]string `json:"avatarUrls"`
    90  }
    91  
    92  type JiraHandler struct {
    93  	MyHttp     httpClient
    94  	JiraServer string
    95  	UserName   string
    96  	Password   string
    97  }
    98  
    99  // JiraHost returns the hostname of the jira service as configured.
   100  func (jiraHandler *JiraHandler) JiraHost() string { return jiraHandler.JiraServer }
   101  
   102  // CreateTicket takes a map of fields to initialize a JIRA ticket with. Returns a response containing the
   103  // new ticket's key, id, and API URL. See the JIRA API documentation for help.
   104  func (jiraHandler *JiraHandler) CreateTicket(fields map[string]interface{}) (*JiraCreateTicketResponse, error) {
   105  	postArgs := struct {
   106  		Fields map[string]interface{} `json:"fields"`
   107  	}{fields}
   108  	apiEndpoint := fmt.Sprintf("https://%v/rest/api/2/issue", jiraHandler.JiraServer)
   109  	res, err := jiraHandler.MyHttp.doPost(apiEndpoint, jiraHandler.UserName, jiraHandler.Password, postArgs)
   110  	if res != nil {
   111  		defer res.Body.Close()
   112  	}
   113  	if err != nil {
   114  		return nil, errors.WithStack(err)
   115  	}
   116  	if res.StatusCode >= 300 || res.StatusCode < 200 {
   117  		msg, _ := ioutil.ReadAll(res.Body)
   118  		return nil, errors.Errorf("HTTP request returned unexpected status `%v`: %v", res.Status, string(msg))
   119  	}
   120  
   121  	ticketInfo := &JiraCreateTicketResponse{}
   122  	if err := json.NewDecoder(res.Body).Decode(ticketInfo); err != nil {
   123  		return nil, errors.Wrap(err, "Unable to decode http body")
   124  	}
   125  	return ticketInfo, nil
   126  }
   127  
   128  // UpdateTicket sets the given fields of the ticket with the given key. Returns any errors JIRA returns.
   129  func (jiraHandler *JiraHandler) UpdateTicket(key string, fields map[string]interface{}) error {
   130  	apiEndpoint := fmt.Sprintf("https://%v/rest/api/2/issue/%v", jiraHandler.JiraServer, url.QueryEscape(key))
   131  	putArgs := struct {
   132  		Fields map[string]interface{} `json:"fields"`
   133  	}{fields}
   134  	res, err := jiraHandler.MyHttp.doPut(apiEndpoint, jiraHandler.UserName, jiraHandler.Password, putArgs)
   135  	if res != nil {
   136  		defer res.Body.Close()
   137  	}
   138  	if err != nil {
   139  		return errors.WithStack(err)
   140  	}
   141  	if res.StatusCode >= 300 || res.StatusCode < 200 {
   142  		msg, _ := ioutil.ReadAll(res.Body)
   143  		return errors.Errorf("HTTP request returned unexpected status `%v`: %v", res.Status, string(msg))
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  // GetJIRATicket returns the ticket with the given key.
   150  func (jiraHandler *JiraHandler) GetJIRATicket(key string) (*JiraTicket, error) {
   151  	apiEndpoint := fmt.Sprintf("https://%v/rest/api/latest/issue/%v", jiraHandler.JiraServer, url.QueryEscape(key))
   152  
   153  	res, err := jiraHandler.MyHttp.doGet(apiEndpoint, jiraHandler.UserName, jiraHandler.Password)
   154  	if res != nil {
   155  		defer res.Body.Close()
   156  	}
   157  	if err != nil {
   158  		return nil, errors.WithStack(err)
   159  	}
   160  
   161  	if res == nil {
   162  		return nil, errors.Errorf("HTTP results are nil even though err was nil")
   163  	}
   164  
   165  	if res.StatusCode >= 300 || res.StatusCode < 200 {
   166  		return nil, errors.Errorf("HTTP request returned unexpected status `%v`", res.Status)
   167  	}
   168  
   169  	body, err := ioutil.ReadAll(res.Body)
   170  	if err != nil {
   171  		return nil, errors.Wrap(err, "Unable to read http body")
   172  	}
   173  
   174  	ticket := &JiraTicket{}
   175  	err = json.Unmarshal(body, ticket)
   176  	if err != nil {
   177  		return nil, errors.WithStack(err)
   178  	}
   179  
   180  	return ticket, nil
   181  }
   182  
   183  // JQLSearch runs the given JQL query against the given jira instance and returns
   184  // the results in a JiraSearchResults
   185  func (jiraHandler *JiraHandler) JQLSearch(query string, startAt, maxResults int) (*JiraSearchResults, error) {
   186  	apiEndpoint := fmt.Sprintf("https://%v/rest/api/latest/search?jql=%v&startAt=%d&maxResults=%d", jiraHandler.JiraServer, url.QueryEscape(query), startAt, maxResults)
   187  
   188  	res, err := jiraHandler.MyHttp.doGet(apiEndpoint, jiraHandler.UserName, jiraHandler.Password)
   189  	if err != nil {
   190  		return nil, errors.WithStack(err)
   191  	}
   192  	if res == nil {
   193  		return nil, errors.Errorf("HTTP results are nil even though err was not nil")
   194  	}
   195  
   196  	if res.StatusCode >= 300 || res.StatusCode < 200 {
   197  		return nil, errors.Errorf("HTTP request returned unexpected status `%v`", res.Status)
   198  	}
   199  
   200  	defer res.Body.Close()
   201  
   202  	body, err := ioutil.ReadAll(res.Body)
   203  	if err != nil {
   204  		return nil, errors.Wrap(err, "Unable to read http body")
   205  	}
   206  
   207  	results := &JiraSearchResults{}
   208  	err = json.Unmarshal(body, results)
   209  	if err != nil {
   210  		return nil, errors.WithStack(err)
   211  	}
   212  
   213  	return results, nil
   214  }
   215  
   216  // JQLSearchAll performs repeated JQL searches until the query has been exhausted
   217  func (jiraHandler *JiraHandler) JQLSearchAll(query string) ([]JiraTicket, error) {
   218  	allIssues := []JiraTicket{}
   219  
   220  	index := 0
   221  	ticketsLeft := math.MaxInt32
   222  
   223  	for ticketsLeft > 0 {
   224  		nextResult, err := jiraHandler.JQLSearch(query, index, -1)
   225  		if err != nil {
   226  			return []JiraTicket{}, errors.WithStack(err)
   227  		}
   228  
   229  		numReturned := nextResult.MaxResults
   230  		ticketsLeft = nextResult.Total - (index + numReturned)
   231  		index = numReturned + index + 1
   232  
   233  		allIssues = append(allIssues, nextResult.Issues...)
   234  	}
   235  
   236  	return allIssues, nil
   237  
   238  }
   239  
   240  func NewJiraHandler(server string, user string, password string) JiraHandler {
   241  	return JiraHandler{
   242  		liveHttp{},
   243  		server,
   244  		user,
   245  		password,
   246  	}
   247  }