Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 282 additions & 0 deletions backend/plugins/azuredevops_go/api/azuredevops/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azuredevops

import (
"encoding/json"
"fmt"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/azuredevops_go/models"
"io"
"net/http"
"net/url"
"strconv"
"time"
)

const apiVersion = "7.1"
const maxPageSize = 100

type Client struct {
c http.Client

apiClient plugin.ApiClient
connection *models.AzuredevopsConnection
url string
}

func NewClient(con *models.AzuredevopsConnection, apiClient plugin.ApiClient, url string) Client {
return Client{
c: http.Client{
Timeout: 2 * time.Second,
},
connection: con,
url: url,
apiClient: apiClient,
}
}

func (c *Client) GetUserProfile() (Profile, errors.Error) {
var p Profile
endpoint, err := url.JoinPath(c.url, "/_apis/profile/profiles/me")
if err != nil {
return Profile{}, errors.Internal.Wrap(err, "failed to join user profile path")
}

res, err := c.doGet(endpoint)
if err != nil {
return Profile{}, errors.Internal.Wrap(err, "failed to read user accounts")
}

if res.StatusCode == 302 || res.StatusCode == 401 {
return Profile{}, errors.Unauthorized.New("failed to read user profile")
}

defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
return Profile{}, errors.Internal.Wrap(err, "failed to read response body")
}

if err := json.Unmarshal(resBody, &p); err != nil {
panic(err)
}
return p, nil
}

func (c *Client) GetUserAccounts(memberId string) (AccountResponse, errors.Error) {
var a AccountResponse
endpoint := fmt.Sprintf(c.url+"/_apis/accounts?memberId=%s", memberId)
res, err := c.doGet(endpoint)
if err != nil {
return nil, errors.Internal.Wrap(err, "failed to read user accounts")
}

if res.StatusCode == 302 || res.StatusCode == 401 {
return nil, errors.Unauthorized.New("failed to read user accounts")
}

defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, errors.Internal.Wrap(err, "failed to read response body")
}

if err := json.Unmarshal(resBody, &a); err != nil {
return nil, errors.Internal.Wrap(err, "failed to read unmarshal response body")
}
return a, nil
}

func (c *Client) doGet(url string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

if err = c.connection.GetAccessTokenAuthenticator().SetupAuthentication(req); err != nil {
return nil, errors.Internal.Wrap(err, "failed to authorize the request using the plugin connection")
}
return http.DefaultClient.Do(req)
}

type GetProjectsArgs struct {
// (optional) Pagination
*OffsetPagination

OrgId string
}

func (c *Client) GetProjects(args GetProjectsArgs) ([]Project, errors.Error) {
query := url.Values{}
query.Set("api-version", apiVersion)

var top, skip int
top = maxPageSize
skip = 0
if args.OffsetPagination != nil {
top = args.Top
skip = args.Skip
}

var data struct {
Count int `json:"count"`
Projects []Project `json:"value"`
}

var projects []Project

for {
query.Set("$top", strconv.Itoa(top))
query.Set("$skip", strconv.Itoa(skip))

path := fmt.Sprintf("%s/_apis/projects", args.OrgId)
res, err := c.apiClient.Get(path, query, nil)
if err != nil {
return nil, err
}
err = api.UnmarshalResponse(res, &data)
if err != nil {
return nil, err
}

projects = append(projects, data.Projects...)

if data.Count < top {
return projects, nil
}

skip += top
}
}

type GetRepositoriesArgs struct {
OrgId string
ProjectId string
}

func (c *Client) GetRepositories(args GetRepositoriesArgs) ([]Repository, errors.Error) {
query := url.Values{}
query.Set("api-version", apiVersion)

var data struct {
Repos []Repository `json:"value"`
}

path := fmt.Sprintf("%s/%s/_apis/git/repositories", args.OrgId, args.ProjectId)
res, err := c.apiClient.Get(path, query, nil)
if err != nil {
return nil, err
}

switch res.StatusCode {
case 401:
fallthrough
case 403:
return nil, errors.Unauthorized.New("failed to authorize the '.../_apis/git/repositories' request using the plugin connection")
case 404:
return nil, errors.NotFound.New("failed to find requested resource on '.../_apis/git/repositories'")
default:
}

err = api.UnmarshalResponse(res, &data)
if err != nil {
return nil, err
}

return data.Repos, nil
}

type GetServiceEndpointsArgs struct {
ProjectId string
OrgId string
}

func (c *Client) GetServiceEndpoints(args GetServiceEndpointsArgs) ([]ServiceEndpoint, errors.Error) {
query := url.Values{}
query.Set("api-version", apiVersion)

path := fmt.Sprintf("%s/%s/_apis/serviceendpoint/endpoints/", args.OrgId, args.ProjectId)
res, err := c.apiClient.Get(path, query, nil)
if err != nil {
return nil, err
}

switch res.StatusCode {
case 401:
fallthrough
case 403:
return nil, errors.Unauthorized.New("failed to authorize the '.../serviceendpoint/endpoints' request using the plugin connection")
case 404:
return nil, errors.NotFound.New("failed to find requested resource on '.../serviceendpoint/endpoints'")
default:
}

var data struct {
ServiceEndpoints []ServiceEndpoint `json:"value"`
}

err = api.UnmarshalResponse(res, &data)
if err != nil {
return nil, err
}
return data.ServiceEndpoints, nil
}

type GetRemoteRepositoriesArgs struct {
ProjectId string
OrgId string
Provider string
// (optional) Service Endpoint to filter for
ServiceEndpoint string
}

func (c *Client) GetRemoteRepositories(args GetRemoteRepositoriesArgs) ([]RemoteRepository, error) {
query := url.Values{}
query.Set("api-version", apiVersion)
if args.ServiceEndpoint != "" {
query.Set("serviceEndpointId", args.ServiceEndpoint)
}

var repos []RemoteRepository
var response struct {
Repository []RemoteRepository `json:"repositories"`
}

for {
path := fmt.Sprintf("%s/%s/_apis/sourceProviders/%s/repositories/", args.OrgId, args.ProjectId, args.Provider)
res, err := c.apiClient.Get(path, query, nil)
if err != nil {
return nil, errors.Internal.Wrap(err, "failed to read remote repositories")
}
err = api.UnmarshalResponse(res, &response)
if err != nil {
return nil, errors.Internal.Wrap(err, "failed to unmarshal remote repositories response")
}

repos = append(repos, response.Repository...)
contToken := res.Header.Get("X-Ms-Continuationtoken")
if contToken == "" {
return repos, nil
}

query.Set("continuationToken", contToken)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package api
package azuredevops

import (
"bytes"
Expand Down Expand Up @@ -89,8 +89,8 @@ func TestRetrieveUserProfile(t *testing.T) {
},
}

client := newVsClient(conn, ts.URL)
p, err := client.UserProfile()
client := NewClient(conn, nil, ts.URL)
p, err := client.GetUserProfile()
if err != nil && err.GetType().GetHttpCode() != code {
t.Errorf("User Profile API Response = %d; want: %d", err.GetType().GetHttpCode(), code)
}
Expand Down
Loading