pkg/server/meta/yourtrackClient.go (179 lines of code) (raw):
package meta
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"mime/multipart"
"net/http"
"net/url"
"time"
"github.com/JetBrains/ij-perf-report-aggregator/pkg/server/auth"
)
type YoutrackClient struct {
youTrackUrl string
youtrackToken string
}
type YoutrackProject struct {
ID string `json:"id"`
}
type YoutrackIssue struct {
ID string `json:"id"`
IDReadable string `json:"idReadable"`
}
type CustomFieldValue struct {
Name string `json:"name"`
}
type CustomField struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"$type"`
Value any `json:"value"`
}
type Visibility struct {
PermittedGroups []auth.YTUser `json:"permittedGroups"`
PermittedUsers []auth.YTUser `json:"permittedUsers"`
Type string `json:"$type"`
}
type Tag struct {
Name string `json:"name"`
ID string `json:"id"`
Type string `json:"$type"`
}
type CreateIssueInfo struct {
Summary string `json:"summary"`
Description string `json:"description"`
Project YoutrackProject `json:"project"`
Reporter *auth.YTUser `json:"reporter,omitempty"`
Visibility Visibility `json:"visibility"`
CustomFields []CustomField `json:"customFields"`
Tags []Tag `json:"tags,omitempty"`
}
func NewYoutrackClient(youTrackUrl, youtrackToken string) *YoutrackClient {
return &YoutrackClient{
youTrackUrl: youTrackUrl,
youtrackToken: youtrackToken,
}
}
func (client *YoutrackClient) CreateIssue(ctx context.Context, info CreateIssueInfo) (*YoutrackIssue, error) {
body, err := json.Marshal(info)
if err != nil {
return nil, fmt.Errorf("error marshaling info: %w", err)
}
slog.Info("Create issue data:", "info", string(body))
var jsonData map[string]any
if err := json.Unmarshal(body, &jsonData); err != nil {
slog.Error("error unmarshalling JSON:", "error", err)
} else {
slog.Info("JSON is valid")
}
headers := map[string]string{
"Content-Type": "application/json",
}
responseData, err := client.fetchFromYouTrack(ctx, "/api/issues?fields=id,idReadable", "POST", bytes.NewBuffer(body), headers)
if err != nil {
return nil, fmt.Errorf("error creating info: %w", err)
}
var issue YoutrackIssue
if err := json.Unmarshal(responseData, &issue); err != nil {
return nil, fmt.Errorf("error unmarshalling issue: %w", err)
}
return &issue, nil
}
func (client *YoutrackClient) SearchIssuesByLabel(ctx context.Context, label string) ([]YoutrackIssue, error) {
encodedLabel := url.QueryEscape(fmt.Sprintf("{%s}", label))
responseData, err := client.fetchFromYouTrack(ctx, "/api/issues?query=tag:"+encodedLabel, "GET", nil, map[string]string{"Accept": "application/json"})
if err != nil {
return nil, fmt.Errorf("error fetching issues: %w", err)
}
var issues []YoutrackIssue
if err := json.Unmarshal(responseData, &issues); err != nil {
return nil, fmt.Errorf("error unmarshalling issues: %w", err)
}
return issues, nil
}
func (client *YoutrackClient) GetCustomFields(ctx context.Context, projectId string) ([]CustomField, error) {
responseData, err := client.fetchFromYouTrack(ctx, fmt.Sprintf("/api/admin/projects/%s/customFields?fields=value(name)", projectId), "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("error fetching custom fields: %w", err)
}
var customFields []CustomField
if err := json.Unmarshal(responseData, &customFields); err != nil {
return nil, fmt.Errorf("error unmarshalling custom fields: %w", err)
}
return customFields, nil
}
func (client *YoutrackClient) UploadAttachment(ctx context.Context, issueId string, file []byte, fileName string) error {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", fileName)
if err != nil {
return fmt.Errorf("error creating form file: %w", err)
}
_, err = io.Copy(part, bytes.NewReader(file))
if err != nil {
return fmt.Errorf("error copying file data: %w", err)
}
writer.Close()
err = client.waitIssueIsCreated(ctx, issueId)
if err != nil {
return fmt.Errorf("issue was not created: %w", err)
}
_, err = client.fetchFromYouTrack(ctx, fmt.Sprintf("/api/issues/%s/attachments", issueId), "POST", &body, map[string]string{"Content-Type": writer.FormDataContentType()})
if err != nil {
return fmt.Errorf("error uploading attachment: %w", err)
}
return nil
}
func (client *YoutrackClient) fetchFromYouTrack(ctx context.Context, endpoint string, method string, body io.Reader, headers map[string]string) ([]byte, error) {
youtrackUrl := fmt.Sprintf("%s%s", client.youTrackUrl, endpoint)
log.Printf("Youtrack url: %s\n", youtrackUrl)
req, err := http.NewRequestWithContext(ctx, method, youtrackUrl, body)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+client.youtrackToken)
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error performing request: %w", err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
bodyString := string(bodyBytes)
return nil, fmt.Errorf("request failed with status: %s. Body: %s", resp.Status, bodyString)
}
return bodyBytes, nil
}
func (client *YoutrackClient) waitIssueIsCreated(ctx context.Context, issueId string) error {
var responseData []byte
var err error
var issue YoutrackIssue
for range 5 {
responseData, err = client.fetchFromYouTrack(ctx, fmt.Sprintf("/api/issues/%s?fields=id,idReadable", issueId), http.MethodGet, nil, map[string]string{
"Content-Type": "application/json",
})
if err == nil {
if err = json.Unmarshal(responseData, &issue); err == nil {
break
}
err = fmt.Errorf("error unmarshalling issue: %w", err)
} else {
err = fmt.Errorf("error fetching from YouTrack: %w", err)
}
time.Sleep(3 * time.Second)
}
if err != nil {
return fmt.Errorf("checking whether the issue is created failed after 5 retries: %w", err)
}
return nil
}