backend/helpers/pluginhelper/api/remote_api_helper.go (359 lines of code) (raw):

/* 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 ( gocontext "context" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "github.com/apache/incubator-devlake/core/log" "github.com/apache/incubator-devlake/core/context" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" "github.com/go-playground/validator/v10" ) type RemoteScopesChild struct { Type string `json:"type"` ParentId *string `json:"parentId"` Id string `json:"id"` Name string `json:"name"` FullName string `json:"fullName"` Data interface{} `json:"data"` } type RemoteQueryData struct { Page int `json:"page"` PerPage int `json:"per_page"` CustomInfo string `json:"custom"` Tag string `json:"tag"` Search []string } type FirstPageTokenOutput struct { PageToken string `json:"pageToken"` } 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"` } // RemoteApiHelper is used to write the CURD of connection type RemoteApiHelper[Conn plugin.ApiConnection, Scope plugin.ToolLayerScope, ApiScope plugin.ApiScope, Group plugin.ApiGroup] struct { basicRes context.BasicRes validator *validator.Validate connHelper *ConnectionApiHelper httpClientCache map[string]*ApiClient logger log.Logger } // NewRemoteHelper creates a ScopeHelper for connection management func NewRemoteHelper[Conn plugin.ApiConnection, Scope plugin.ToolLayerScope, ApiScope plugin.ApiScope, Group plugin.ApiGroup]( basicRes context.BasicRes, vld *validator.Validate, connHelper *ConnectionApiHelper, ) *RemoteApiHelper[Conn, Scope, ApiScope, Group] { if vld == nil { vld = validator.New() } if connHelper == nil { return nil } return &RemoteApiHelper[Conn, Scope, ApiScope, Group]{ basicRes: basicRes, validator: vld, connHelper: connHelper, httpClientCache: make(map[string]*ApiClient), logger: basicRes.GetLogger(), } } type NoRemoteGroupResponse struct { } func (NoRemoteGroupResponse) GroupId() string { return "" } func (NoRemoteGroupResponse) GroupName() string { return "" } type BaseRemoteGroupResponse struct { Id string Name string } func (g BaseRemoteGroupResponse) GroupId() string { return g.Id } func (g BaseRemoteGroupResponse) GroupName() string { return g.Name } const remoteScopesPerPage int = 100 const ( TypeGroup string = "group" // group is just like a directory or a folder, that holds some scopes. TypeScope string = "scope" // scope, sometimes we call it project. But scope is a more standard noun. TypeMixed string = "mixed" ) func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) GetApiClient(connection plugin.CacheableConnection) (*ApiClient, errors.Error) { key := connection.GetHash() // empty key means no connection reuse if key == "" { r.logger.Info("No api client reuse") return NewApiClientFromConnection(gocontext.TODO(), r.basicRes, connection) } if client, ok := r.httpClientCache[key]; ok { r.logger.Info("Reused api client") return client, nil } r.logger.Info("Creating new api client") newClient, err := NewApiClientFromConnection(gocontext.TODO(), r.basicRes, connection) if err != nil { return nil, err } r.httpClientCache[key] = newClient return newClient, nil } func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) ProxyApiGet(conn plugin.CacheableConnection, path string, query url.Values) (*plugin.ApiResourceOutput, errors.Error) { apiClient, err := r.GetApiClient(conn) if err != nil { return nil, err } resp, err := apiClient.Get(path, query, nil) if err != nil { return nil, err } defer resp.Body.Close() body, err := errors.Convert01(io.ReadAll(resp.Body)) if err != nil { return nil, err } // verify response body is json var tmp interface{} err = errors.Convert(json.Unmarshal(body, &tmp)) if err != nil { return nil, err } return &plugin.ApiResourceOutput{Status: resp.StatusCode, Body: json.RawMessage(body)}, nil } // PrepareFirstPageToken prepares the first page token func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) PrepareFirstPageToken(customInfo string) (*plugin.ApiResourceOutput, errors.Error) { outputBody := &FirstPageTokenOutput{} pageToken, err := getPageTokenFromPageData(&RemoteQueryData{ Page: 1, PerPage: remoteScopesPerPage, CustomInfo: customInfo, Tag: TypeGroup, }) if err != nil { return nil, err } outputBody.PageToken = pageToken return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil } func (r *RemoteApiHelper[Conn, Scope, ApiScope, ApiGroup]) GetRemoteScopesOutput( input *plugin.ApiResourceInput, getter func(basicRes context.BasicRes, groupId string, queryData *RemoteQueryData, connection Conn) (*RemoteScopesOutput, errors.Error), ) (*RemoteScopesOutput, errors.Error) { connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64)) if err != nil || connectionId == 0 { return nil, errors.BadInput.New("invalid connectionId") } var connection Conn err = r.connHelper.First(&connection, input.Params) if err != nil { r.logger.Error(err, "find connection: %d", connectionId) return nil, err } groupId := input.Query.Get("groupId") pageToken := input.Query.Get("pageToken") queryData, err := getPageDataFromPageTokenWithTag(pageToken, TypeMixed) if err != nil { r.logger.Error(err, "get page data from page token") return nil, err } resp, err := getter(r.basicRes, groupId, queryData, connection) if err != nil { r.logger.Error(err, "call getter") return nil, err } queryData.Page += 1 resp.NextPageToken, err = getPageTokenFromPageData(queryData) if err != nil { r.logger.Error(err, "get next page token") return nil, err } if len(resp.Children) < queryData.PerPage { // there are no more pages resp.NextPageToken = "" } return resp, nil } // GetScopesFromRemote gets the scopes from api func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) GetScopesFromRemote( input *plugin.ApiResourceInput, getGroup func(basicRes context.BasicRes, gid string, queryData *RemoteQueryData, connection Conn) ([]Group, errors.Error), getScope func(basicRes context.BasicRes, gid string, queryData *RemoteQueryData, connection Conn) ([]ApiScope, errors.Error), ) (*plugin.ApiResourceOutput, errors.Error) { connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64)) if err != nil || connectionId == 0 { return nil, errors.BadInput.New("invalid connectionId") } var connection Conn err = r.connHelper.First(&connection, input.Params) if err != nil { return nil, err } // get groupId and pageData groupId := input.Query.Get("groupId") pageToken := input.Query.Get("pageToken") queryData, err := getPageDataFromPageToken(pageToken) if err != nil { return nil, errors.BadInput.New("failed to get page token") } outputBody := &RemoteScopesOutput{} // list groups part if queryData.Tag == TypeGroup { var resBody []Group if getGroup != nil { resBody, err = getGroup(r.basicRes, groupId, queryData, connection) } if err != nil { return nil, err } // if len(resBody) == 0, will skip the following steps, this will happen in some plugins which don't have group // append group to output for _, group := range resBody { child := RemoteScopesChild{ Type: TypeGroup, Id: group.GroupId(), Name: group.GroupName(), // don't need to save group into data Data: nil, } child.ParentId = &groupId if *child.ParentId == "" { child.ParentId = nil } outputBody.Children = append(outputBody.Children, child) } // check groups count if len(resBody) < queryData.PerPage { queryData.Tag = TypeScope queryData.Page = 1 queryData.PerPage = queryData.PerPage - len(resBody) } } // list projects part if queryData.Tag == TypeScope && getScope != nil { var resBody []ApiScope resBody, err = getScope(r.basicRes, groupId, queryData, connection) if err != nil { return nil, err } // append project to output for _, project := range resBody { scope := project.ConvertApiScope() child := RemoteScopesChild{ Type: TypeScope, Id: scope.ScopeId(), Name: scope.ScopeName(), FullName: scope.ScopeFullName(), Data: &scope, } child.ParentId = &groupId if *child.ParentId == "" { child.ParentId = nil } outputBody.Children = append(outputBody.Children, child) } // check scopes count if len(resBody) < queryData.PerPage { queryData = nil } } // get the next page token outputBody.NextPageToken = "" if queryData != nil { queryData.Page += 1 outputBody.NextPageToken, err = getPageTokenFromPageData(queryData) if err != nil { return nil, err } } return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil } func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) SearchRemoteScopes(input *plugin.ApiResourceInput, searchScope func(basicRes context.BasicRes, queryData *RemoteQueryData, connection Conn) ([]ApiScope, errors.Error), ) (*plugin.ApiResourceOutput, errors.Error) { connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64)) if err != nil || connectionId == 0 { return nil, errors.BadInput.New("invalid connectionId") } var connection Conn err = r.connHelper.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 err1 != 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 = remoteScopesPerPage } 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])) } } queryData := &RemoteQueryData{ Page: p, PerPage: ps, Search: search, } var resBody []ApiScope resBody, err = searchScope(r.basicRes, queryData, connection) if err != nil { return nil, err } outputBody := &SearchRemoteScopesOutput{} // append project to output for _, project := range resBody { scope := project.ConvertApiScope() child := RemoteScopesChild{ Type: TypeScope, Id: scope.ScopeId(), ParentId: nil, Name: scope.ScopeName(), FullName: scope.ScopeFullName(), Data: scope, } outputBody.Children = append(outputBody.Children, child) } outputBody.Page = p outputBody.PageSize = ps return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil } func getPageTokenFromPageData(pageData *RemoteQueryData) (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) (*RemoteQueryData, errors.Error) { return getPageDataFromPageTokenWithTag(pageToken, TypeGroup) } func getPageDataFromPageTokenWithTag(pageToken string, queryTag string) (*RemoteQueryData, errors.Error) { if pageToken == "" { return &RemoteQueryData{ Page: 1, PerPage: remoteScopesPerPage, Tag: queryTag, }, 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 := &RemoteQueryData{} 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 }