blob: 904e41cc8405a5db1d2146e97e5ccd9e01635eec [file] [log] [blame]
/*
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"`
Name string `json:"name"`
Data interface{} `json:"data"`
}
type RemoteScopesOutput struct {
Children []RemoteScopesChild `json:"children"`
NextPageToken string `json:"nextPageToken"`
}
type SearchRemoteScopesOutput struct {
Children []RemoteScopesChild `json:"children"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type PageData struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Tag string `json:"tag"`
}
type GroupResponse struct {
Id int `json:"id" group:"id"`
WebUrl string `json:"web_url"`
Name string `json:"name" group:"name"`
Path string `json:"path"`
Description string `json:"description"`
FullName string `json:"full_name"`
FullPath string `json:"full_path"`
ParentId *int `json:"parent_id"`
}
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 query string false "group ID"
// @Param pageToken query string false "page Token"
// @Success 200 {object} RemoteScopesOutput
// @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]
rgid := gid
if len(rgid) >= 6 {
if rgid[:6] == "group:" {
rgid = rgid[6:]
}
}
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
}
if gid == "" {
query.Set("top_level_only", "true")
res, err = apiClient.Get("groups", query, nil)
} else {
res, err = apiClient.Get(fmt.Sprintf("groups/%s/subgroups", rgid), query, nil)
}
if err != nil {
return nil, err
}
resBody := []GroupResponse{}
err = api.UnmarshalResponse(res, &resBody)
if err != nil {
return nil, err
}
// append group to output
for _, group := range resBody {
child := RemoteScopesChild{
Type: TypeGroup,
Id: "group:" + strconv.Itoa(group.Id),
Name: group.Name,
// 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 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", rgid), query, nil)
}
if err != nil {
return nil, err
}
resBody := []tasks.GitlabApiProject{}
err = api.UnmarshalResponse(res, &resBody)
if err != nil {
return nil, err
}
// append project to output
for _, project := range resBody {
child := RemoteScopesChild{
Type: TypeProject,
Id: strconv.Itoa(project.GitlabId),
Name: project.Name,
Data: tasks.ConvertProject(&project),
}
child.ParentId = &gid
if *child.ParentId == "" {
child.ParentId = nil
}
outputBody.Children = append(outputBody.Children, child)
}
// check project count
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 query string false "search"
// @Param page query int false "page number"
// @Param pageSize query int false "page size per page"
// @Success 200 {object} SearchRemoteScopesOutput
// @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{""}
}
var p int
var err1 error
page, ok := input.Query["page"]
if !ok || len(page) == 0 {
p = 1
} else {
p, err1 = strconv.Atoi(page[0])
if err != nil {
return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to Atoi page:%s", page[0]))
}
}
var ps int
pageSize, ok := input.Query["pageSize"]
if !ok || len(pageSize) == 0 {
ps = GitlabRemoteScopesPerPage
} else {
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
}
outputBody := &SearchRemoteScopesOutput{}
// append project to output
for _, project := range resBody {
child := RemoteScopesChild{
Type: TypeProject,
Id: strconv.Itoa(project.GitlabId),
ParentId: nil,
Name: project.PathWithNamespace,
Data: tasks.ConvertProject(&project),
}
outputBody.Children = append(outputBody.Children, child)
}
outputBody.Page = p
outputBody.PageSize = ps
return &plugin.ApiResourceOutput{Body: outputBody, 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
}