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 }