pkg/client/jira/jira_adapter.go (155 lines of code) (raw):
package jira
import (
"context"
"errors"
"fmt"
"net/http"
url "net/url"
"strconv"
"strings"
"github.com/andygrunwald/go-jira"
ctrl "sigs.k8s.io/controller-runtime"
)
var log = ctrl.Log.WithName("gojira_adapter")
var ErrNotFound = errors.New("404")
// IssueTypeMeta represents issue metadata response from Jira.
// It is not full representation of response, only fields that are used in codebase-operator.
// See https://docs.atlassian.com/software/jira/docs/api/REST/9.4.5/#api/2/issue-getCreateIssueMetaFields.
type IssueTypeMeta struct {
FieldID string `json:"fieldId,omitempty"`
AllowedValues []IssueTypeMetaAllowedValue `json:"allowedValues,omitempty"`
}
// IssueTypeMetaAllowedValue represents allowed value for issue type metadata response from Jira.
type IssueTypeMetaAllowedValue struct {
Name string `json:"name,omitempty"`
}
// List represents pagination response from Jira.
type List[T any] struct {
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
IsLat bool `json:"isLast"`
Values []T `json:"values"`
}
type GoJiraAdapter struct {
client jira.Client
}
func (a *GoJiraAdapter) Connected() (bool, error) {
log.V(2).Info("start Connected method")
ctx := context.Background()
user, _, err := a.client.User.GetSelfWithContext(ctx)
if err != nil {
return false, fmt.Errorf("failed to fetch jira user: %w", err)
}
return user != nil, nil
}
func (a *GoJiraAdapter) GetIssue(ctx context.Context, issueId string) (*jira.Issue, error) {
issue, _, err := a.client.Issue.GetWithContext(ctx, issueId, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch jira issue: %w", err)
}
return issue, nil
}
func (a *GoJiraAdapter) GetProjectInfo(issueId string) (*jira.Project, error) {
logv := log.WithValues("issue", issueId)
logv.V(2).Info("start GetProjectInfo method.")
ctx := context.Background()
issueResp, httpResp, err := a.client.Issue.GetWithContext(ctx, issueId, nil)
if httpResp != nil && httpResp.StatusCode == http.StatusNotFound {
return nil, ErrNotFound
}
if err != nil {
if strings.Contains(err.Error(), "404") {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to fetch jira issue: %w", err)
}
logv.V(2).Info("project info has been fetched.", "id", issueResp.Fields.Project.ID)
return &issueResp.Fields.Project, nil
}
func (a *GoJiraAdapter) CreateFixVersionValue(ctx context.Context, projectId int, versionName string) error {
logv := ctrl.LoggerFrom(ctx).WithValues("project id", projectId, "version name", versionName)
logv.Info("Start CreateFixVersionValue method")
_, _, err := a.client.Version.CreateWithContext(ctx, &jira.Version{
Name: versionName,
ProjectID: projectId,
})
if err != nil {
return fmt.Errorf("failed to create jira version component: %w", err)
}
logv.Info("Fix version has been created")
return nil
}
func (a *GoJiraAdapter) CreateComponentValue(ctx context.Context, projectId int, componentName string) error {
logv := ctrl.LoggerFrom(ctx).WithValues("project id", projectId, "component name", componentName)
logv.Info("Start CreateComponentValue method")
project, _, err := a.client.Project.Get(strconv.Itoa(projectId))
if err != nil {
return fmt.Errorf("failed to fetch jira project: %w", err)
}
_, _, err = a.client.Component.CreateWithContext(ctx, &jira.CreateComponentOptions{
Name: componentName,
Project: project.Key,
ProjectID: projectId,
})
if err != nil {
return fmt.Errorf("failed to create jira component: %w", err)
}
logv.Info("Component value has been created")
return nil
}
func (a *GoJiraAdapter) ApplyTagsToIssue(issue string, tags map[string]interface{}) error {
logv := log.WithValues("issue", issue, "tags", tags)
logv.V(2).Info("start ApplyTagsToIssue method.")
ctx := context.Background()
if _, err := a.client.Issue.UpdateIssueWithContext(ctx, issue, tags); err != nil {
return fmt.Errorf("failed to update jira issue: %w", err)
}
logv.Info("end ApplyTagsToIssue method.")
return nil
}
func (a *GoJiraAdapter) CreateIssueLink(issueId, title, link string) error {
logv := log.WithValues("issueId", issueId, "title", title, "url", link)
logv.V(2).Info("start CreateIssueLink method.")
ctx := context.Background()
remoteLink := &jira.RemoteLink{
Relationship: "links to",
Object: &jira.RemoteLinkObject{
Title: title,
URL: link,
Icon: &jira.RemoteLinkIcon{
Url16x16: "https://raw.githubusercontent.com/KubeRocketCI/docs/main/static/img/kuberocketci.png",
},
},
}
req, err := a.client.NewRequestWithContext(ctx, "POST", fmt.Sprintf("rest/api/2/issue/%v/remotelink", issueId), remoteLink)
if err != nil {
return fmt.Errorf("failed to create HTTP request to jira: %w", err)
}
_, err = a.client.Do(req, nil)
if err != nil {
return fmt.Errorf("failed to perform HTTP request to jira: %w", err)
}
log.Info("end CreateIssueLink method.")
return nil
}
// GetIssueTypeMeta returns issue type meta for given project and issue type.
// Map key is issue type meta fieldId, value is IssueTypeMeta.
// API doc: https://docs.atlassian.com/software/jira/docs/api/REST/9.4.5/#api/2/issue-getCreateIssueMetaFields.
func (a *GoJiraAdapter) GetIssueTypeMeta(ctx context.Context, projectID, issueTypeID string) (map[string]IssueTypeMeta, error) {
u := url.URL{
Path: fmt.Sprintf("rest/api/2/issue/createmeta/%s/issuetypes/%s", projectID, issueTypeID),
}
uv := url.Values{}
// it is crucial to set maxResults to a high value, otherwise we will not get all the fields(default is 50)
uv.Add("maxResults", "1000")
req, err := a.client.NewRequestWithContext(
ctx,
http.MethodGet,
u.String(),
nil,
)
if err != nil {
return nil, fmt.Errorf("failed to create GetIssueTypeMeta HTTP request to jira: %w", err)
}
issueTypeMeta := &List[IssueTypeMeta]{}
if _, err = a.client.Do(req, issueTypeMeta); err != nil {
return nil, fmt.Errorf("failed to perform GetIssueTypeMeta HTTP request to jira: %w", err)
}
issueTypeMetaMap := make(map[string]IssueTypeMeta, len(issueTypeMeta.Values))
for _, issueMeta := range issueTypeMeta.Values {
issueTypeMetaMap[issueMeta.FieldID] = issueMeta
}
return issueTypeMetaMap, nil
}