From ec91c0f965323478411b6af06f5a4f287bb286ee Mon Sep 17 00:00:00 2001 From: Sandro Mello Date: Fri, 20 Dec 2024 08:10:14 -0300 Subject: [PATCH] Create Jira issue using the service desk api (#613) --- gateway/api/session/session.go | 12 +++-- gateway/jira/requestsapi.go | 97 ++++++++++++++++++++++++++++++++++ gateway/jira/types.go | 63 ++++++++++++++++++++++ 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 gateway/jira/requestsapi.go diff --git a/gateway/api/session/session.go b/gateway/api/session/session.go index 4bd7eea6..92a463f0 100644 --- a/gateway/api/session/session.go +++ b/gateway/api/session/session.go @@ -177,19 +177,21 @@ func Post(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) return } - resp, err := jira.CreateIssue(issueTemplate, jiraConfig, jiraFields) + resp, err := jira.CreateCustomerRequest(issueTemplate, jiraConfig, jiraFields) + // resp, err := jira.CreateIssue(issueTemplate, jiraConfig, jiraFields) if err != nil { log.Error(err) c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) return } err = models.UpdateSessionIntegrationMetadata(ctx.OrgID, sessionID, map[string]any{ - "jira_issue_key": resp.Key, - "jira_issue_url": fmt.Sprintf("%s/browse/%s", jiraConfig.URL, resp.Key), + "jira_issue_key": resp.IssueKey, + "jira_issue_url": resp.Links.Agent, }) if err != nil { - log.Errorf("failed updating session with jira issue (%s), reason=%v", resp.Key, err) - c.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("failed updating session with jira issue %s", resp.Key)}) + log.Errorf("failed updating session with jira issue (%s), reason=%v", resp.IssueKey, err) + c.JSON(http.StatusInternalServerError, gin.H{ + "message": fmt.Sprintf("failed updating session with jira issue %s", resp.IssueKey)}) return } } diff --git a/gateway/jira/requestsapi.go b/gateway/jira/requestsapi.go new file mode 100644 index 00000000..16f2e9c5 --- /dev/null +++ b/gateway/jira/requestsapi.go @@ -0,0 +1,97 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/hoophq/hoop/common/log" + "github.com/hoophq/hoop/gateway/models" +) + +func CreateCustomerRequest(tmpl *models.JiraIssueTemplate, config *models.JiraIntegration, fields CustomFields) (*RequestResponse, error) { + serviceDeskID, err := fetchServiceDeskID(config, tmpl.ProjectKey) + if err != nil { + return nil, err + } + + if _, hasSummary := fields["summary"]; !hasSummary { + fields["summary"] = "Hoop Session" + } + issue := IssueFieldsV2[CustomFields]{ + ServiceDeskID: serviceDeskID, + RequestTypeID: tmpl.IssueTypeName, + IsAdfRequest: false, + IssueFieldValues: IssueFieldValues[CustomFields]{fields}, + } + issuePayload, err := json.Marshal(issue) + if err != nil { + return nil, fmt.Errorf("failed encoding issue payload, reason=%v", err) + } + log.Infof("creating jira issue request with payload: %v", string(issuePayload)) + apiURL := fmt.Sprintf("%s/rest/servicedeskapi/request", config.URL) + req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(issuePayload)) + if err != nil { + return nil, fmt.Errorf("failed creating request, reason=%v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.SetBasicAuth(config.User, config.APIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed creating jira issue, reason=%v", err) + } + defer resp.Body.Close() + if resp.StatusCode != 201 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unable to create jira issue, status=%v, body=%v", + resp.StatusCode, string(body)) + } + var response RequestResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed decoding jira issue response, reason=%v", err) + } + return &response, nil +} + +func fetchServiceDeskID(config *models.JiraIntegration, projectKey string) (string, error) { + // temporary to avoid having to paginate + if val := os.Getenv("JIRA_SERVICE_DESK_ID"); val != "" { + return val, nil + } + apiURL := fmt.Sprintf("%s/rest/servicedeskapi/servicedesk?limit=100", config.URL) + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return "", fmt.Errorf("failed creating service desk http request, reason=%v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.SetBasicAuth(config.User, config.APIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed performing service desk http request for %s, reason=%v", projectKey, err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("unable to list service desk resources, api-url=%v, status=%v, body=%v", + apiURL, resp.StatusCode, string(body)) + } + var obj ServiceDesk + if err := json.NewDecoder(resp.Body).Decode(&obj); err != nil { + return "", fmt.Errorf("failed decoding service desk payload, reason=%v", err) + } + for _, val := range obj.Values { + if val.ProjectKey == projectKey { + return val.ID, nil + } + } + if !obj.IsLastPage { + return "", fmt.Errorf("unable to find service desk id for %v, pagination not implemented", projectKey) + } + log.Warnf("unable to find project key %v, values=%v", projectKey, obj.Values) + return "", fmt.Errorf("unable to find project key %v", projectKey) +} diff --git a/gateway/jira/types.go b/gateway/jira/types.go index 78de7f26..7381a6bc 100644 --- a/gateway/jira/types.go +++ b/gateway/jira/types.go @@ -28,6 +28,34 @@ type IssueResponse struct { Self string `json:"self"` } +type RequestLinks struct { + JiraRest string `json:"jiraRest"` + Web string `json:"web"` + Agent string `json:"agent"` + Self string `json:"self"` +} + +type RequestResponse struct { + IssueID string `json:"issueId"` + IssueKey string `json:"issueKey"` + Links RequestLinks `json:"_links"` +} + +type ServiceDeskValue struct { + ID string `json:"id"` + ProjectID string `json:"projectId"` + ProjectName string `json:"projectName"` + ProjectKey string `json:"projectKey"` +} + +type ServiceDesk struct { + Start int `json:"start"` + Size int `json:"size"` + Limit int `json:"limit"` + IsLastPage bool `json:"isLastPage"` + Values []ServiceDeskValue `json:"values"` +} + type Project struct { Key string `json:"key"` } @@ -69,6 +97,41 @@ func (A IssueFields[T]) MarshalJSON() ([]byte, error) { return resp, nil } +type IssueFieldValues[T any] struct { + CustomFields T `json:"requestFieldValues"` +} + +type IssueFieldsV2[T any] struct { + ServiceDeskID string `json:"serviceDeskId"` + RequestTypeID string `json:"requestTypeId"` + IsAdfRequest bool `json:"isAdfRequest"` + + IssueFieldValues IssueFieldValues[T] `json:"-"` +} + +func (A IssueFieldsV2[T]) MarshalJSON() ([]byte, error) { + type ResponseAlias IssueFieldsV2[types.Nil] + resp, err := json.Marshal(ResponseAlias{ + ServiceDeskID: A.ServiceDeskID, + RequestTypeID: A.RequestTypeID, + IsAdfRequest: A.IsAdfRequest, + }) + if err != nil { + return nil, err + } + + data, err := json.Marshal(A.IssueFieldValues) + if err != nil { + return nil, err + } + if bytes.Equal(data, []byte(`{}`)) { + return resp, nil + } + v := append(resp[1:len(resp)-1], byte(',')) + resp = slices.Insert(data, 1, v...) + return resp, nil +} + func loadDefaultPresetFields(s storagev2types.Session) map[string]string { return map[string]string{ "session.id": s.ID,