pkg/server/meta/teamcityClient.go (251 lines of code) (raw):

package meta import ( "bytes" "context" "encoding/json" "encoding/xml" "fmt" "io" "net/http" "net/url" ) type TeamCityClient struct { teamCityURL string authToken string } type Test struct { ID string `xml:"id,attr"` Name string `xml:"name,attr"` Href string `xml:"href,attr"` } type Tests struct { Count int `xml:"count,attr"` Test []Test `xml:"test"` } type TeamCityAttachmentInfo struct { CurrentBuildId int `json:"currentBuildId"` PreviousBuildId *int `json:"previousBuildId"` } type Files struct { XMLName xml.Name `xml:"files"` Files []File `xml:"file"` } type File struct { Name string `xml:"name,attr"` } func NewTeamCityClient(teamCityURL, authToken string) *TeamCityClient { return &TeamCityClient{ teamCityURL: teamCityURL, authToken: authToken, } } func (client *TeamCityClient) makeRequest(ctx context.Context, endpoint string, headers map[string]string) (*http.Response, error) { myUrl := fmt.Sprintf("%s%s", client.teamCityURL, endpoint) fmt.Printf("Requesting URL: %s\n", myUrl) req, err := http.NewRequestWithContext(ctx, http.MethodGet, myUrl, http.NoBody) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+client.authToken) for key, value := range headers { req.Header.Set(key, value) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return resp, fmt.Errorf("request failed: %s", resp.Status) } return resp, nil } func (client *TeamCityClient) getArtifactChildren(ctx context.Context, buildId int, testName string) ([]string, error) { endpoint := fmt.Sprintf("/app/rest/builds/id:%d/artifacts/children/%s", buildId, testName) resp, err := client.makeRequest(ctx, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var files Files err = xml.Unmarshal(body, &files) if err != nil { return nil, err } children := make([]string, 0, len(files.Files)) for _, child := range files.Files { children = append(children, child.Name) } return children, nil } func (client *TeamCityClient) downloadArtifact(ctx context.Context, buildId int, filePath string) ([]byte, error) { endpoint := fmt.Sprintf("/app/rest/builds/id:%d/artifacts/content/%s", buildId, filePath) resp, err := client.makeRequest(ctx, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return body, nil } func (client *TeamCityClient) getTestHistoryUrl(ctx context.Context, testName string) (string, error) { endpoint := "/app/rest/tests?locator=name:" + url.QueryEscape(testName) resp, err := client.makeRequest(ctx, endpoint, nil) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } var tests Tests err = xml.Unmarshal(body, &tests) if err != nil { return "", err } if len(tests.Test) == 0 { return "", fmt.Errorf("test not found: %s", testName) } return fmt.Sprintf("%s/test/%s/?currentProjectId=ijplatform", client.teamCityURL, tests.Test[0].ID), nil } // Build represents the root element of the XML structure type Build struct { XMLName xml.Name `xml:"build"` BuildType BuildType `xml:"buildType"` Properties Properties `xml:"properties"` } // BuildType represents the buildType element type BuildType struct { ID string `xml:"id,attr"` } // Properties represents the properties element type Properties struct { Property []Property `xml:"property"` } // Property represents an individual property element type Property struct { Name string `xml:"name,attr"` Value string `xml:"value,attr"` } type BuildResponse struct { XMLName xml.Name `xml:"build"` WebURL string `xml:"webUrl,attr"` } type BuildInfo struct { BuildTypeId string `json:"buildTypeId"` Number string `json:"number"` BranchName string `json:"branchName"` StartDate string `json:"startDate"` } type Change struct { Version string `json:"version"` } type Changes struct { Change []Change `json:"change"` } func (client *TeamCityClient) getBuildType(ctx context.Context, buildID string) (string, error) { res, err := client.makeRequest(ctx, "/app/rest/builds/id:"+buildID, map[string]string{"Accept": "application/json"}) if err != nil { return "", err } defer res.Body.Close() var build BuildInfo if err := json.NewDecoder(res.Body).Decode(&build); err != nil { return "", fmt.Errorf("failed to decode changes response: %w", err) } return build.BuildTypeId, nil } func (client *TeamCityClient) getBuildCounter(ctx context.Context, buildID string) (string, error) { res, err := client.makeRequest(ctx, "/app/rest/builds/id:"+buildID, map[string]string{"Accept": "application/json"}) if err != nil { return "", err } defer res.Body.Close() var build BuildInfo if err := json.NewDecoder(res.Body).Decode(&build); err != nil { return "", fmt.Errorf("failed to decode build response: %w", err) } return build.Number, nil } type CommitRevisions struct { FirstCommit string `json:"firstCommit"` LastCommit string `json:"lastCommit"` } func (client *TeamCityClient) getBuildInfo(ctx context.Context, buildID string) (*BuildInfo, error) { res, err := client.makeRequest(ctx, "/app/rest/builds/id:"+buildID, map[string]string{"Accept": "application/json"}) if err != nil { return nil, err } defer res.Body.Close() var build BuildInfo if err := json.NewDecoder(res.Body).Decode(&build); err != nil { return nil, fmt.Errorf("failed to decode build info response: %w", err) } return &build, nil } func (client *TeamCityClient) getChanges(ctx context.Context, buildID string) (*CommitRevisions, error) { res, err := client.makeRequest(ctx, "/app/rest/changes?locator=build:(id:"+buildID+")&count=10000", map[string]string{"Accept": "application/json"}) if err != nil { return nil, err } defer res.Body.Close() var changes Changes if err := json.NewDecoder(res.Body).Decode(&changes); err != nil { return nil, fmt.Errorf("failed to decode changes response: %w", err) } if len(changes.Change) == 0 { return nil, fmt.Errorf("no changes found for build %s", buildID) } revisions := &CommitRevisions{ LastCommit: changes.Change[0].Version, FirstCommit: changes.Change[len(changes.Change)-1].Version, } return revisions, nil } func (client *TeamCityClient) startBuild(ctx context.Context, buildId string, params map[string]string) (*string, error) { endpoint := "/app/rest/buildQueue" properties := Properties{ Property: make([]Property, 0, len(params)), } for key, value := range params { properties.Property = append(properties.Property, Property{ Name: key, Value: value, }) } build := Build{ BuildType: BuildType{ID: buildId}, Properties: properties, } myUrl := fmt.Sprintf("%s%s", client.teamCityURL, endpoint) fmt.Printf("Requesting URL: %s\n", myUrl) buildXml, err := xml.Marshal(build) if err != nil { return nil, fmt.Errorf("error marshalling build %v: %w", build, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, myUrl, bytes.NewBuffer(buildXml)) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+client.authToken) req.Header.Set("Content-Type", "application/xml") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("error sending request: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed: %s", resp.Status) } body := resp.Body all, err := io.ReadAll(body) defer body.Close() if err != nil { return nil, fmt.Errorf("error reading body: %w", err) } var buildResponse BuildResponse err = xml.Unmarshal(all, &buildResponse) if err != nil { return nil, fmt.Errorf("error unmarshaling XML: %w", err) } return &buildResponse.WebURL, nil }