This is an automated email from the ASF dual-hosted git repository.
likyh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new f9a68dbc7 Gitlab remote api (#4397)
f9a68dbc7 is described below
commit f9a68dbc745888c4abe978744f6460b58549a4dd
Author: mappjzc <[email protected]>
AuthorDate: Mon Feb 13 19:50:26 2023 +0800
Gitlab remote api (#4397)
* feat: gitlab v11 surpport
Add ExtractApiPipelineDetails
Add CollectApiPipelineDetails
Add ExtractAccountDetails
Add CollectAccountDetails
Add ExtractApiMergeRequestDetails
Add CollectApiMergeRequestDetails
Add NewApiClientFromConnectionWithTest
updated gitlab connection creating prograss
Nddtfjiang <[email protected]>
* fix: fix for review
fix for review
Nddtfjiang <[email protected]>
* feat: gitlab connection adding
Add PrepareApiClient for gitlab connection
Add Version for gitlab connection
Nddtfjiang <[email protected]>
* fix: add apiClient data
Add ApiClient data for gitlab
Nddtfjiang <[email protected]>
* feat: add is detail required
Add IsDetailRequired to _tool_gitlab_merge_requests
Add IsDetailRequired to _tool_gitlab_pipelines
Nddtfjiang <[email protected]>
* feat: add remote api
Add RemoteScopes
Add SearchRemoteScopes
fix some review for gitlab v11
Nddtfjiang <[email protected]>
* fix: fix for review
Add with_shared
Add top_level_only
Add ignore not top_level
Nddtfjiang <[email protected]>
---
backend/plugins/gitlab/api/remote.go | 379 +++++++++++++++++++++
backend/plugins/gitlab/api/scope.go | 8 +-
backend/plugins/gitlab/impl/impl.go | 6 +
.../plugins/gitlab/tasks/mr_detail_collector.go | 17 +-
backend/plugins/gitlab/tasks/mr_extractor.go | 6 +-
.../gitlab/tasks/pipeline_detail_collector.go | 8 +-
6 files changed, 407 insertions(+), 17 deletions(-)
diff --git a/backend/plugins/gitlab/api/remote.go
b/backend/plugins/gitlab/api/remote.go
new file mode 100644
index 000000000..3a6a8d51c
--- /dev/null
+++ b/backend/plugins/gitlab/api/remote.go
@@ -0,0 +1,379 @@
+/*
+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 api
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+
+ "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/gitlab/models"
+ "github.com/apache/incubator-devlake/plugins/gitlab/tasks"
+)
+
+type RemoteScopesChild struct {
+ Type string `json:"type"`
+ ParentId *string `json:"parentId"`
+ Id string `json:"id"`
+ Data interface{} `json:"data"`
+}
+
+type RemoteScopesOutput struct {
+ Children []RemoteScopesChild `json:"children"`
+ NextPageToken string `json:"nextPageToken"`
+}
+
+type PageData struct {
+ Page int `json:"page"`
+ PerPage int `json:"per_page"`
+ Tag string `json:"tag"`
+}
+
+type GroupResponse struct {
+ Id int `json:"id"`
+ WebUrl string `json:"web_url"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Description string `json:"description"`
+ Visibility string `json:"visibility"`
+ LfsEnabled bool `json:"lfs_enabled"`
+ AvatarUrl string `json:"avatar_url"`
+ RequestAccessEnabled bool `json:"request_access_enabled"`
+ FullName string `json:"full_name"`
+ FullPath string `json:"full_path"`
+ ParentId *int `json:"parent_id"`
+ LdapCN string `json:"ldap_cn"`
+ LdapAccess string `json:"ldap_access"`
+}
+
+const GitlabRemoteScopesPerPage int = 100
+const TypeProject string = "scope"
+const TypeGroup string = "group"
+
+// RemoteScopes list all available scope for users
+// @Summary list all available scope for users
+// @Description list all available scope for users
+// @Tags plugins/gitlab
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param groupId body string false "group ID"
+// @Param pageToken body string false "page Token"
+// @Success 200 {object} []models.GitlabProject
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/gitlab/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
+ connectionId, _ := extractParam(input.Params)
+ if connectionId == 0 {
+ return nil, errors.BadInput.New("invalid connectionId")
+ }
+
+ connection := &models.GitlabConnection{}
+ err := connectionHelper.First(connection, input.Params)
+ if err != nil {
+ return nil, err
+ }
+
+ groupId, ok := input.Query["groupId"]
+ if !ok || len(groupId) == 0 {
+ groupId = []string{""}
+ }
+
+ pageToken, ok := input.Query["pageToken"]
+ if !ok || len(pageToken) == 0 {
+ pageToken = []string{""}
+ }
+
+ // get gid and pageData
+ gid := groupId[0]
+ pageData, err := GetPageDataFromPageToken(pageToken[0])
+ if err != nil {
+ return nil, errors.BadInput.New("failed to get paget token")
+ }
+
+ // create api client
+ apiClient, err := api.NewApiClientFromConnection(context.TODO(),
basicRes, connection)
+ if err != nil {
+ return nil, err
+ }
+
+ var res *http.Response
+ outputBody := &RemoteScopesOutput{}
+
+ // list groups part
+ if pageData.Tag == TypeGroup {
+ query, err := GetQueryFromPageData(pageData)
+ if err != nil {
+ return nil, err
+ }
+ query.Set("top_level_only", "true")
+
+ if gid == "" {
+ res, err = apiClient.Get("groups", query, nil)
+ } else {
+ res, err =
apiClient.Get(fmt.Sprintf("groups/%s/subgroups", gid), query, nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ resBody := []GroupResponse{}
+ err = api.UnmarshalResponse(res, &resBody)
+
+ // append group to output
+ for _, group := range resBody {
+ child := RemoteScopesChild{
+ Type: TypeGroup,
+ Id: strconv.Itoa(group.Id),
+ // don't need to save group into data
+ Data: nil,
+ }
+
+ // ignore not top_level
+ if group.ParentId == nil {
+ if gid != "" {
+ continue
+ }
+ } else {
+ if strconv.Itoa(*group.ParentId) != gid {
+ continue
+ }
+ }
+
+ // ignore self
+ if gid == child.Id {
+ continue
+ }
+
+ child.ParentId = &gid
+ if *child.ParentId == "" {
+ child.ParentId = nil
+ }
+
+ outputBody.Children = append(outputBody.Children, child)
+ }
+
+ // check groups count
+ if err != nil {
+ return nil, err
+ }
+ if len(resBody) < pageData.PerPage {
+ pageData.Tag = TypeProject
+ pageData.Page = 1
+ pageData.PerPage = pageData.PerPage - len(resBody)
+ }
+ }
+
+ // list projects part
+ if pageData.Tag == TypeProject {
+ query, err := GetQueryFromPageData(pageData)
+ if err != nil {
+ return nil, err
+ }
+ if gid == "" {
+ res, err =
apiClient.Get(fmt.Sprintf("users/%d/projects",
apiClient.GetData(models.GitlabApiClientData_UserId)), query, nil)
+ } else {
+ query.Set("with_shared", "false")
+ res, err =
apiClient.Get(fmt.Sprintf("/groups/%s/projects", gid), query, nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ resBody := []tasks.GitlabApiProject{}
+ err = api.UnmarshalResponse(res, &resBody)
+
+ // append project to output
+ for _, project := range resBody {
+ child := RemoteScopesChild{
+ Type: TypeProject,
+ Id: strconv.Itoa(project.CreatorId),
+ Data: tasks.ConvertProject(&project),
+ }
+ child.ParentId = &gid
+ if *child.ParentId == "" {
+ child.ParentId = nil
+ }
+
+ outputBody.Children = append(outputBody.Children, child)
+ }
+
+ // check project count
+ if err != nil {
+ return nil, err
+ }
+ if len(resBody) < pageData.PerPage {
+ pageData = nil
+ }
+ }
+
+ // get the next page token
+ outputBody.NextPageToken = ""
+ if pageData != nil {
+ pageData.Page += 1
+ pageData.PerPage = GitlabRemoteScopesPerPage
+
+ outputBody.NextPageToken, err =
GetPageTokenFromPageData(pageData)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &plugin.ApiResourceOutput{Body: outputBody, Status:
http.StatusOK}, nil
+}
+
+// SearchRemoteScopes use the Search API and only return project
+// @Summary use the Search API and only return project
+// @Description use the Search API and only return project
+// @Tags plugins/gitlab
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param search body string false "group ID"
+// @Param page body int false "page number"
+// @Param pageSize body int false "page size per page"
+// @Success 200 {object} []models.GitlabProject
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /plugins/gitlab/connections/{connectionId}/search-remote-scopes
[GET]
+func SearchRemoteScopes(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
+ connectionId, _ := extractParam(input.Params)
+ if connectionId == 0 {
+ return nil, errors.BadInput.New("invalid connectionId")
+ }
+
+ connection := &models.GitlabConnection{}
+ err := connectionHelper.First(connection, input.Params)
+ if err != nil {
+ return nil, err
+ }
+
+ search, ok := input.Query["search"]
+ if !ok || len(search) == 0 {
+ search = []string{""}
+ }
+
+ page, ok := input.Query["page"]
+ if !ok || len(page) == 0 {
+ page = []string{""}
+ }
+
+ p, err1 := strconv.Atoi(page[0])
+ if err1 != nil {
+ return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to
Atoi page:%s", page[0]))
+ }
+
+ pageSize, ok := input.Query["pageSize"]
+ if !ok || len(pageSize) == 0 {
+ pageSize = []string{""}
+ }
+ ps, err1 := strconv.Atoi(pageSize[0])
+ if err1 != nil {
+ return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to
Atoi pageSize:%s", pageSize[0]))
+ }
+
+ // create api client
+ apiClient, err := api.NewApiClientFromConnection(context.TODO(),
basicRes, connection)
+ if err != nil {
+ return nil, err
+ }
+
+ // set query
+ query, err := GetQueryForSearchProject(search[0], p, ps)
+ if err != nil {
+ return nil, err
+ }
+
+ // request search
+ res, err := apiClient.Get("search", query, nil)
+ if err != nil {
+ return nil, err
+ }
+ resBody := []tasks.GitlabApiProject{}
+ err = api.UnmarshalResponse(res, &resBody)
+ if err != nil {
+ return nil, err
+ }
+
+ // set projects return
+ projects := []models.GitlabProject{}
+ for _, project := range resBody {
+ projects = append(projects, *tasks.ConvertProject(&project))
+ }
+
+ return &plugin.ApiResourceOutput{Body: projects, Status:
http.StatusOK}, nil
+}
+
+func GetPageTokenFromPageData(pageData *PageData) (string, errors.Error) {
+ // Marshal json
+ pageTokenDecode, err := json.Marshal(pageData)
+ if err != nil {
+ return "", errors.Default.Wrap(err, fmt.Sprintf("Marshal
pageToken failed %+v", pageData))
+ }
+
+ // Encode pageToken Base64
+ return base64.StdEncoding.EncodeToString(pageTokenDecode), nil
+}
+
+func GetPageDataFromPageToken(pageToken string) (*PageData, errors.Error) {
+ if pageToken == "" {
+ return &PageData{
+ Page: 1,
+ PerPage: GitlabRemoteScopesPerPage,
+ Tag: "group",
+ }, nil
+ }
+
+ // Decode pageToken Base64
+ pageTokenDecode, err := base64.StdEncoding.DecodeString(pageToken)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, fmt.Sprintf("decode
pageToken failed %s", pageToken))
+ }
+ // Unmarshal json
+ pt := &PageData{}
+ err = json.Unmarshal(pageTokenDecode, pt)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, fmt.Sprintf("json
Unmarshal pageTokenDecode failed %s", pageTokenDecode))
+ }
+
+ return pt, nil
+}
+
+func GetQueryFromPageData(pageData *PageData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", pageData.Page))
+ query.Set("per_page", fmt.Sprintf("%v", pageData.PerPage))
+ return query, nil
+}
+
+func GetQueryForSearchProject(search string, page int, perPage int)
(url.Values, errors.Error) {
+ query, err := GetQueryFromPageData(&PageData{Page: page, PerPage:
perPage})
+ if err != nil {
+ return nil, err
+ }
+ query.Set("search", search)
+ query.Set("scope", "projects")
+
+ return query, nil
+}
diff --git a/backend/plugins/gitlab/api/scope.go
b/backend/plugins/gitlab/api/scope.go
index 58ea0814c..6af3c4b1e 100644
--- a/backend/plugins/gitlab/api/scope.go
+++ b/backend/plugins/gitlab/api/scope.go
@@ -18,15 +18,15 @@ limitations under the License.
package api
import (
+ "net/http"
+ "strconv"
+ "time"
+
"github.com/apache/incubator-devlake/core/dal"
"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/gitlab/models"
- "net/http"
- "strconv"
- "time"
-
"github.com/mitchellh/mapstructure"
)
diff --git a/backend/plugins/gitlab/impl/impl.go
b/backend/plugins/gitlab/impl/impl.go
index ecb399ee9..6cdc2d4be 100644
--- a/backend/plugins/gitlab/impl/impl.go
+++ b/backend/plugins/gitlab/impl/impl.go
@@ -247,6 +247,12 @@ func (p Gitlab) ApiResources()
map[string]map[string]plugin.ApiResourceHandler {
"GET": api.GetScope,
"PATCH": api.UpdateScope,
},
+ "connections/:connectionId/remote-scopes": {
+ "GET": api.RemoteScopes,
+ },
+ "connections/:connectionId/search-remote-scopes": {
+ "GET": api.SearchRemoteScopes,
+ },
"connections/:connectionId/scopes": {
"GET": api.GetScopeList,
"PUT": api.PutScope,
diff --git a/backend/plugins/gitlab/tasks/mr_detail_collector.go
b/backend/plugins/gitlab/tasks/mr_detail_collector.go
index 3d3ff9ee9..64a959bf8 100644
--- a/backend/plugins/gitlab/tasks/mr_detail_collector.go
+++ b/backend/plugins/gitlab/tasks/mr_detail_collector.go
@@ -18,6 +18,7 @@ limitations under the License.
package tasks
import (
+ "net/url"
"reflect"
"github.com/apache/incubator-devlake/core/dal"
@@ -49,13 +50,15 @@ func CollectApiMergeRequestDetails(taskCtx
plugin.SubTaskContext) errors.Error {
}
err = collectorWithState.InitCollector(helper.ApiCollectorArgs{
- ApiClient: data.ApiClient,
- PageSize: 100,
- Incremental: false,
- Input: iterator,
- UrlTemplate: "projects/{{ .Params.ProjectId
}}/merge_requests/{{ .Input.Iid }}",
- Query: GetQuery,
- GetTotalPages: GetTotalPagesFromResponse,
+ ApiClient: data.ApiClient,
+ Incremental: false,
+ Input: iterator,
+ UrlTemplate: "projects/{{ .Params.ProjectId
}}/merge_requests/{{ .Input.Iid }}",
+ Query: func(reqData *helper.RequestData) (url.Values,
errors.Error) {
+ query := url.Values{}
+ query.Set("with_stats", "true")
+ return query, nil
+ },
ResponseParser: GetOneRawMessageFromResponse,
})
if err != nil {
diff --git a/backend/plugins/gitlab/tasks/mr_extractor.go
b/backend/plugins/gitlab/tasks/mr_extractor.go
index 13d07af30..bd557f86e 100644
--- a/backend/plugins/gitlab/tasks/mr_extractor.go
+++ b/backend/plugins/gitlab/tasks/mr_extractor.go
@@ -170,12 +170,8 @@ func ExtractApiMergeRequests(taskCtx
plugin.SubTaskContext) errors.Error {
return errors.Convert(err)
}
- err = extractor.Execute()
- if err != nil {
- return err
- }
+ return extractor.Execute()
- return nil
}
func convertMergeRequest(mr *MergeRequestRes) (*models.GitlabMergeRequest,
errors.Error) {
diff --git a/backend/plugins/gitlab/tasks/pipeline_detail_collector.go
b/backend/plugins/gitlab/tasks/pipeline_detail_collector.go
index 69977ae3f..4465df09e 100644
--- a/backend/plugins/gitlab/tasks/pipeline_detail_collector.go
+++ b/backend/plugins/gitlab/tasks/pipeline_detail_collector.go
@@ -20,6 +20,7 @@ package tasks
import (
"net/url"
"reflect"
+ "time"
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
@@ -44,6 +45,11 @@ func CollectApiPipelineDetails(taskCtx
plugin.SubTaskContext) errors.Error {
return err
}
+ tickInterval, err := helper.CalcTickInterval(200, 1*time.Minute)
+ if err != nil {
+ return err
+ }
+
incremental := collectorWithState.IsIncremental()
iterator, err := GetPipelinesIterator(taskCtx, collectorWithState)
@@ -55,7 +61,7 @@ func CollectApiPipelineDetails(taskCtx plugin.SubTaskContext)
errors.Error {
err = collectorWithState.InitCollector(helper.ApiCollectorArgs{
RawDataSubTaskArgs: *rawDataSubTaskArgs,
ApiClient: data.ApiClient,
- PageSize: 100,
+ MinTickInterval: &tickInterval,
Input: iterator,
Incremental: incremental,
UrlTemplate: "projects/{{ .Params.ProjectId
}}/pipelines/{{ .Input.GitlabId }}",