You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2023/02/21 12:43:48 UTC

[incubator-devlake] branch main updated: Bamboo scopes and remote api (#4461)

This is an automated email from the ASF dual-hosted git repository.

warren 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 dca268280 Bamboo scopes and remote api (#4461)
dca268280 is described below

commit dca268280fae478f5433a2589e34489458961c0c
Author: mappjzc <zh...@merico.dev>
AuthorDate: Tue Feb 21 20:43:42 2023 +0800

    Bamboo scopes and remote api (#4461)
    
    * feat: bamboo blueprint
    
    Add bamboo blueprint.
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix for merge
    
    fix for merge
    
    Nddtfjiang <zh...@merico.dev>
    
    * feat: add scopes and remote
    
    Add scopes and remote api for bamboo
    
    Nddtfjiang <zh...@merico.dev>
    
    * feat: scope and remote api for bamboo
    
    Add scope and remote api for bamboo
    
    Nddtfjiang <zh...@merico.dev>
---
 backend/plugins/bamboo/api/blueprint_V200_test.go  |  11 +-
 backend/plugins/bamboo/api/blueprint_v200.go       |   8 +-
 backend/plugins/bamboo/api/remote.go               | 302 +++++++++++++++++++++
 backend/plugins/{sonarqube => bamboo}/api/scope.go | 153 +++++++----
 backend/plugins/bamboo/impl/impl.go                |  15 +
 backend/plugins/bamboo/models/job.go               |  21 +-
 .../migrationscripts/20230216_add_init_tables.go   |   2 +-
 .../models/migrationscripts/archived/project.go    |  16 +-
 backend/plugins/bamboo/models/plan.go              |  39 ++-
 backend/plugins/bamboo/models/project.go           |  19 ++
 backend/plugins/bamboo/tasks/job_extractor.go      |   4 +-
 backend/plugins/bamboo/tasks/plan_extractor.go     |   4 +-
 backend/plugins/sonarqube/api/scope.go             |   4 +-
 13 files changed, 496 insertions(+), 102 deletions(-)

diff --git a/backend/plugins/bamboo/api/blueprint_V200_test.go b/backend/plugins/bamboo/api/blueprint_V200_test.go
index 3622b93c2..e9f1f4b43 100644
--- a/backend/plugins/bamboo/api/blueprint_V200_test.go
+++ b/backend/plugins/bamboo/api/blueprint_V200_test.go
@@ -30,6 +30,7 @@ import (
 	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 	"github.com/apache/incubator-devlake/helpers/unithelper"
 	"github.com/apache/incubator-devlake/plugins/bamboo/models"
+	"github.com/apache/incubator-devlake/plugins/bamboo/tasks"
 	"github.com/go-playground/validator/v10"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -94,13 +95,17 @@ func TestMakeDataSourcePipelinePlanV200(t *testing.T) {
 
 	var expectRepoId = "bamboo:BambooProject:1:TEST"
 
-	var testSubTaskMeta = []plugin.SubTaskMeta{}
+	var testSubTaskMeta = []plugin.SubTaskMeta{
+		tasks.ConvertProjectsMeta,
+	}
 
 	var expectPlans = plugin.PipelinePlan{
 		{
 			{
-				Plugin:   "bamboo",
-				Subtasks: []string{},
+				Plugin: "bamboo",
+				Subtasks: []string{
+					tasks.ConvertProjectsMeta.Name,
+				},
 				Options: map[string]interface{}{
 					"connectionId":         uint64(1),
 					"projectKey":           testKey,
diff --git a/backend/plugins/bamboo/api/blueprint_v200.go b/backend/plugins/bamboo/api/blueprint_v200.go
index d627b0973..d7fd92815 100644
--- a/backend/plugins/bamboo/api/blueprint_v200.go
+++ b/backend/plugins/bamboo/api/blueprint_v200.go
@@ -67,7 +67,7 @@ func makeScopeV200(connectionId uint64, scopes []*plugin.BlueprintScopeV200) ([]
 		id := didgen.NewDomainIdGenerator(&models.BambooProject{}).Generate(connectionId, scope.Id)
 
 		// get project from db
-		BambooProject, err := GetprojectByConnectionIdAndscopeId(connectionId, scope.Id)
+		BambooProject, err := GetProjectByConnectionIdAndscopeId(connectionId, scope.Id)
 		if err != nil {
 			return nil, err
 		}
@@ -93,7 +93,7 @@ func makePipelinePlanV200(
 		var stage plugin.PipelineStage
 		var err errors.Error
 		// get project
-		project, err := GetprojectByConnectionIdAndscopeId(connection.ID, scope.Id)
+		project, err := GetProjectByConnectionIdAndscopeId(connection.ID, scope.Id)
 		if err != nil {
 			return nil, err
 		}
@@ -127,8 +127,8 @@ func makePipelinePlanV200(
 	return plans, nil
 }
 
-// GetprojectByConnectionIdAndscopeId get tbe project by the connectionId and the scopeId
-func GetprojectByConnectionIdAndscopeId(connectionId uint64, scopeId string) (*models.BambooProject, errors.Error) {
+// GetProjectByConnectionIdAndscopeId get tbe project by the connectionId and the scopeId
+func GetProjectByConnectionIdAndscopeId(connectionId uint64, scopeId string) (*models.BambooProject, errors.Error) {
 	key := scopeId
 	project := &models.BambooProject{}
 	db := basicRes.GetDal()
diff --git a/backend/plugins/bamboo/api/remote.go b/backend/plugins/bamboo/api/remote.go
new file mode 100644
index 000000000..250571390
--- /dev/null
+++ b/backend/plugins/bamboo/api/remote.go
@@ -0,0 +1,302 @@
+/*
+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/bamboo/models"
+)
+
+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"`
+	PageSize int `json:"per_page"`
+}
+
+const BambooRemoteScopesPerPage 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/bamboo
+// @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/bamboo/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.BambooConnection{}
+	err := connectionHelper.First(connection, input.Params)
+	if err != nil {
+		return nil, err
+	}
+
+	pageToken, ok := input.Query["pageToken"]
+	if !ok || len(pageToken) == 0 {
+		pageToken = []string{""}
+	}
+
+	// get pageData
+	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{}
+
+	query := GetQueryFromPageData(pageData)
+
+	res, err = apiClient.Get("/project.json", query, nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	resBody := models.ApiBambooProjectResponse{}
+	err = api.UnmarshalResponse(res, &resBody)
+	if err != nil {
+		return nil, err
+	}
+
+	// append project to output
+	for _, apiProject := range resBody.Projects.Projects {
+		project := &models.BambooProject{}
+		project.Convert(&apiProject)
+		child := RemoteScopesChild{
+			Type:     TypeProject,
+			ParentId: nil,
+			Id:       project.ProjectKey,
+			Name:     project.Name,
+			Data:     project,
+		}
+
+		outputBody.Children = append(outputBody.Children, child)
+	}
+
+	// check project count
+	if len(resBody.Projects.Projects) < pageData.PageSize {
+		pageData = nil
+	}
+
+	// get the next page token
+	outputBody.NextPageToken = ""
+	if pageData != nil {
+		pageData.Page += 1
+		pageData.PageSize = BambooRemoteScopesPerPage
+
+		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/bamboo
+// @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/bamboo/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.BambooConnection{}
+	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 = BambooRemoteScopesPerPage
+	} 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 := GetQueryForSearchProject(search[0], p, ps)
+
+	// request search
+	res, err := apiClient.Get("search/projects.json", query, nil)
+	if err != nil {
+		return nil, err
+	}
+	resBody := models.ApiBambooSearchProjectResponse{}
+	err = api.UnmarshalResponse(res, &resBody)
+	if err != nil {
+		return nil, err
+	}
+
+	outputBody := &SearchRemoteScopesOutput{}
+
+	// append project to output
+	for _, apiResult := range resBody.SearchResults {
+		var project models.BambooProject
+		apiProject, err := GetApiProject(apiResult.SearchEntity.Key, apiClient)
+		if err != nil {
+			return nil, err
+		}
+
+		project.Convert(apiProject)
+		child := RemoteScopesChild{
+			Type:     TypeProject,
+			Id:       project.ProjectKey,
+			ParentId: nil,
+			Name:     project.Name,
+			Data:     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,
+			PageSize: BambooRemoteScopesPerPage,
+		}, 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 {
+	query := url.Values{}
+	query.Set("showEmpty", fmt.Sprintf("%v", true))
+	query.Set("max-result", fmt.Sprintf("%v", pageData.PageSize))
+	query.Set("start-index", fmt.Sprintf("%v", (pageData.Page-1)*pageData.PageSize))
+	return query
+}
+
+func GetQueryForSearchProject(search string, page int, perPage int) url.Values {
+	query := GetQueryFromPageData(&PageData{Page: page, PageSize: perPage})
+	query.Set("searchTerm", search)
+
+	return query
+}
diff --git a/backend/plugins/sonarqube/api/scope.go b/backend/plugins/bamboo/api/scope.go
similarity index 54%
copy from backend/plugins/sonarqube/api/scope.go
copy to backend/plugins/bamboo/api/scope.go
index 063020133..703f20a39 100644
--- a/backend/plugins/sonarqube/api/scope.go
+++ b/backend/plugins/bamboo/api/scope.go
@@ -18,6 +18,7 @@ limitations under the License.
 package api
 
 import (
+	"context"
 	"net/http"
 	"strconv"
 
@@ -25,34 +26,40 @@ import (
 	"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/sonarqube/models"
+	"github.com/apache/incubator-devlake/plugins/bamboo/models"
+	"github.com/mitchellh/mapstructure"
 )
 
+type apiProject struct {
+	models.BambooProject
+	TransformationRuleName string `json:"transformationRuleName,omitempty"`
+}
+
 type req struct {
-	Data []*models.SonarqubeProject `json:"data"`
+	Data []*models.BambooProject `json:"data"`
 }
 
-// PutScope create or update sonarqube project
-// @Summary create or update sonarqube project
-// @Description Create or update sonarqube project
-// @Tags plugins/sonarqube
+// PutScope create or update bamboo project
+// @Summary create or update bamboo project
+// @Description Create or update bamboo project
+// @Tags plugins/bamboo
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
 // @Param scope body req true "json"
-// @Success 200  {object} []models.SonarqubeProject
+// @Success 200  {object} []models.BambooProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/scopes [PUT]
+// @Router /plugins/bamboo/connections/{connectionId}/scopes [PUT]
 func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	connectionId, _ := extractParam(input.Params)
+
 	if connectionId == 0 {
 		return nil, errors.BadInput.New("invalid connectionId")
 	}
 	var projects req
-	// As we need to process *api.Iso8601Time, we need to use DecodeMapStruct instead of mapstructure.Decode
-	err := errors.Convert(api.DecodeMapStruct(input.Body, &projects))
+	err := errors.Convert(mapstructure.Decode(input.Body, &projects))
 	if err != nil {
-		return nil, errors.BadInput.Wrap(err, "decoding Sonarqube project error")
+		return nil, errors.BadInput.Wrap(err, "decoding Bamboo project error")
 	}
 	keeper := make(map[string]struct{})
 	for _, project := range projects.Data {
@@ -69,36 +76,36 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 	}
 	err = basicRes.GetDal().CreateOrUpdate(projects.Data)
 	if err != nil {
-		return nil, errors.Default.Wrap(err, "error on saving SonarqubeProject")
+		return nil, errors.Default.Wrap(err, "error on saving BambooProject")
 	}
 	return &plugin.ApiResourceOutput{Body: projects.Data, Status: http.StatusOK}, nil
 }
 
-// UpdateScope patch to sonarqube project
-// @Summary patch to sonarqube project
-// @Description patch to sonarqube project
-// @Tags plugins/sonarqube
+// UpdateScope patch to bamboo project
+// @Summary patch to bamboo project
+// @Description patch to bamboo project
+// @Tags plugins/bamboo
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param projectKey path string false "project Key"
-// @Param scope body models.SonarqubeProject true "json"
-// @Success 200  {object} models.SonarqubeProject
+// @Param projectKey path int false "project ID"
+// @Param scope body models.BambooProject true "json"
+// @Success 200  {object} models.BambooProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/scopes/{projectKey} [PATCH]
+// @Router /plugins/bamboo/connections/{connectionId}/scopes/{projectKey} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	connectionId, projectKey := extractParam(input.Params)
-	if connectionId*uint64(len(projectKey)) == 0 {
-		return nil, errors.BadInput.New("invalid connectionId or projectKey")
+	if connectionId == 0 || projectKey == "" {
+		return nil, errors.BadInput.New("invalid path params")
 	}
-	var project models.SonarqubeProject
+	var project models.BambooProject
 	err := basicRes.GetDal().First(&project, dal.Where("connection_id = ? AND project_key = ?", connectionId, projectKey))
 	if err != nil {
-		return nil, errors.Default.Wrap(err, "getting SonarqubeProject error")
+		return nil, errors.Default.Wrap(err, "getting BambooProject error")
 	}
 	err = api.DecodeMapStruct(input.Body, &project)
 	if err != nil {
-		return nil, errors.Default.Wrap(err, "patch sonarqube project error")
+		return nil, errors.Default.Wrap(err, "patch bamboo project error")
 	}
 	err = verifyProject(&project)
 	if err != nil {
@@ -106,22 +113,22 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 	}
 	err = basicRes.GetDal().Update(project)
 	if err != nil {
-		return nil, errors.Default.Wrap(err, "error on saving SonarqubeProject")
+		return nil, errors.Default.Wrap(err, "error on saving BambooProject")
 	}
 	return &plugin.ApiResourceOutput{Body: project, Status: http.StatusOK}, nil
 }
 
-// GetScopeList get Sonarqube projects
-// @Summary get Sonarqube projects
-// @Description get Sonarqube projects
-// @Tags plugins/sonarqube
+// GetScopeList get Bamboo projects
+// @Summary get Bamboo projects
+// @Description get Bamboo projects
+// @Tags plugins/bamboo
 // @Param connectionId path int false "connection ID"
 // @Success 200  {object} []apiProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/scopes/ [GET]
+// @Router /plugins/bamboo/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var projects []models.SonarqubeProject
+	var projects []models.BambooProject
 	connectionId, _ := extractParam(input.Params)
 	if connectionId == 0 {
 		return nil, errors.BadInput.New("invalid path params")
@@ -131,38 +138,86 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 	if err != nil {
 		return nil, err
 	}
-
-	return &plugin.ApiResourceOutput{Body: projects, Status: http.StatusOK}, nil
+	var ruleIds []uint64
+	for _, proj := range projects {
+		if proj.TransformationRuleId > 0 {
+			ruleIds = append(ruleIds, proj.TransformationRuleId)
+		}
+	}
+	var rules []models.BambooTransformationRule
+	if len(ruleIds) > 0 {
+		err = basicRes.GetDal().All(&rules, dal.Where("id IN (?)", ruleIds))
+		if err != nil {
+			return nil, err
+		}
+	}
+	names := make(map[uint64]string)
+	for _, rule := range rules {
+		names[rule.ID] = rule.Name
+	}
+	var apiProjects []apiProject
+	for _, proj := range projects {
+		apiProjects = append(apiProjects, apiProject{proj, names[proj.TransformationRuleId]})
+	}
+	return &plugin.ApiResourceOutput{Body: apiProjects, Status: http.StatusOK}, nil
 }
 
-// GetScope get one Sonarqube project
-// @Summary get one Sonarqube project
-// @Description get one Sonarqube project
-// @Tags plugins/sonarqube
+// GetScope get one Bamboo project
+// @Summary get one Bamboo project
+// @Description get one Bamboo project
+// @Tags plugins/bamboo
 // @Param connectionId path int false "connection ID"
-// @Param projectKey path string false "project key"
+// @Param projectKey path int false "project ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
 // @Success 200  {object} apiProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/scopes/{projectKey} [GET]
+// @Router /plugins/bamboo/connections/{connectionId}/scopes/{projectKey} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var project models.SonarqubeProject
+	var project models.BambooProject
 	connectionId, projectKey := extractParam(input.Params)
-	if connectionId*uint64(len(projectKey)) == 0 {
+	if connectionId == 0 || projectKey == "" {
 		return nil, errors.BadInput.New("invalid path params")
 	}
 	db := basicRes.GetDal()
 	err := db.First(&project, dal.Where("connection_id = ? AND project_key = ?", connectionId, projectKey))
-	if db.IsErrorNotFound(err) {
+	if err != nil && db.IsErrorNotFound(err) {
+		var scope models.BambooProject
+		connection := &models.BambooConnection{}
+		err = connectionHelper.First(connection, input.Params)
+		if err != nil {
+			return nil, err
+		}
+		apiClient, err := api.NewApiClientFromConnection(context.TODO(), basicRes, connection)
+		if err != nil {
+			return nil, err
+		}
+
+		apiProject, err := GetApiProject(projectKey, apiClient)
+		if err != nil {
+			return nil, err
+		}
+
+		scope.Convert(apiProject)
+		scope.ConnectionId = connectionId
+		err = db.CreateIfNotExist(&scope)
+		if err != nil {
+			return nil, err
+		}
 		return nil, errors.NotFound.New("record not found")
-	}
-	if err != nil {
+	} else if err != nil {
 		return nil, err
 	}
 
-	return &plugin.ApiResourceOutput{Body: project, Status: http.StatusOK}, nil
+	var rule models.BambooTransformationRule
+	if project.TransformationRuleId > 0 {
+		err = basicRes.GetDal().First(&rule, dal.Where("id = ?", project.TransformationRuleId))
+		if err != nil {
+			return nil, err
+		}
+	}
+	return &plugin.ApiResourceOutput{Body: apiProject{project, rule.Name}, Status: http.StatusOK}, nil
 }
 
 func extractParam(params map[string]string) (uint64, string) {
@@ -171,12 +226,12 @@ func extractParam(params map[string]string) (uint64, string) {
 	return connectionId, projectKey
 }
 
-func verifyProject(project *models.SonarqubeProject) errors.Error {
+func verifyProject(project *models.BambooProject) errors.Error {
 	if project.ConnectionId == 0 {
 		return errors.BadInput.New("invalid connectionId")
 	}
-	if len(project.ProjectKey) == 0 {
-		return errors.BadInput.New("invalid project key")
+	if project.ProjectKey == "" {
+		return errors.BadInput.New("invalid projectKey")
 	}
 	return nil
 }
diff --git a/backend/plugins/bamboo/impl/impl.go b/backend/plugins/bamboo/impl/impl.go
index d8fe3b244..70cb8ede7 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -19,6 +19,7 @@ package impl
 
 import (
 	"fmt"
+
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
@@ -186,6 +187,20 @@ func (p Bamboo) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"PATCH":  api.PatchConnection,
 			"DELETE": api.DeleteConnection,
 		},
+		"connections/:connectionId/scopes": {
+			"GET": api.GetScopeList,
+			"PUT": api.PutScope,
+		},
+		"connections/:connectionId/remote-scopes": {
+			"GET": api.RemoteScopes,
+		},
+		"connections/:connectionId/search-remote-scopes": {
+			"GET": api.SearchRemoteScopes,
+		},
+		"connections/:connectionId/scopes/:projectKey": {
+			"GET":   api.GetScope,
+			"PATCH": api.UpdateScope,
+		},
 	}
 }
 
diff --git a/backend/plugins/bamboo/models/job.go b/backend/plugins/bamboo/models/job.go
index 6ee00dc08..a47a7950a 100644
--- a/backend/plugins/bamboo/models/job.go
+++ b/backend/plugins/bamboo/models/job.go
@@ -37,18 +37,15 @@ type BambooJob struct {
 	common.NoPKModel
 }
 
-func (BambooJob) Convert(apiRes *ApiBambooJob) *BambooJob {
-	b := &BambooJob{
-		JobKey:      apiRes.SearchEntity.Key,
-		Id:          apiRes.Id,
-		Name:        apiRes.SearchEntity.JobName,
-		PlanName:    apiRes.SearchEntity.PlanName,
-		ProjectName: apiRes.SearchEntity.ProjectName,
-		Description: apiRes.SearchEntity.Description,
-		BranchName:  apiRes.SearchEntity.BranchName,
-		Type:        apiRes.SearchEntity.Type,
-	}
-	return b
+func (b *BambooJob) Convert(apiRes *ApiBambooJob) {
+	b.JobKey = apiRes.SearchEntity.Key
+	b.Id = apiRes.Id
+	b.Name = apiRes.SearchEntity.JobName
+	b.PlanName = apiRes.SearchEntity.PlanName
+	b.ProjectName = apiRes.SearchEntity.ProjectName
+	b.Description = apiRes.SearchEntity.Description
+	b.BranchName = apiRes.SearchEntity.BranchName
+	b.Type = apiRes.SearchEntity.Type
 }
 
 func (BambooJob) TableName() string {
diff --git a/backend/plugins/bamboo/models/migrationscripts/20230216_add_init_tables.go b/backend/plugins/bamboo/models/migrationscripts/20230216_add_init_tables.go
index c78946a2e..bfb399c34 100644
--- a/backend/plugins/bamboo/models/migrationscripts/20230216_add_init_tables.go
+++ b/backend/plugins/bamboo/models/migrationscripts/20230216_add_init_tables.go
@@ -48,7 +48,7 @@ func (u *addInitTables) Up(baseRes context.BasicRes) errors.Error {
 }
 
 func (*addInitTables) Version() uint64 {
-	return 20230216205037
+	return 20230221205032
 }
 
 func (*addInitTables) Name() string {
diff --git a/backend/plugins/bamboo/models/migrationscripts/archived/project.go b/backend/plugins/bamboo/models/migrationscripts/archived/project.go
index 0ac9d3b8d..ccabf8ef1 100644
--- a/backend/plugins/bamboo/models/migrationscripts/archived/project.go
+++ b/backend/plugins/bamboo/models/migrationscripts/archived/project.go
@@ -22,14 +22,14 @@ import (
 )
 
 type BambooProject struct {
-	ConnectionId uint64 `gorm:"primaryKey"`
-	ProjectKey   string `gorm:"primaryKey;type:varchar(100)"`
-	Expand       string `json:"expand"`
-	Name         string `gorm:"index;type:varchar(100)"`
-	Description  string `json:"description"`
-	Href         string `json:"link"`
-	Rel          string `gorm:"type:varchar(100)"`
-	archived.NoPKModel
+	ConnectionId         uint64 `json:"connectionId" mapstructure:"connectionId" gorm:"primaryKey"`
+	ProjectKey           string `json:"projectKey" gorm:"primaryKey;type:varchar(256)"`
+	TransformationRuleId uint64 `json:"transformationRuleId,omitempty" mapstructure:"transformationRuleId"`
+	Name                 string `json:"name" gorm:"index;type:varchar(256)"`
+	Description          string `json:"description"`
+	Href                 string `json:"link"`
+	Rel                  string `json:"rel" gorm:"type:varchar(100)"`
+	archived.NoPKModel   `json:"-" mapstructure:"-"`
 }
 
 func (BambooProject) TableName() string {
diff --git a/backend/plugins/bamboo/models/plan.go b/backend/plugins/bamboo/models/plan.go
index 4720f8842..8b35ae4f5 100644
--- a/backend/plugins/bamboo/models/plan.go
+++ b/backend/plugins/bamboo/models/plan.go
@@ -43,27 +43,24 @@ type BambooPlan struct {
 	common.NoPKModel
 }
 
-func (BambooPlan) Convert(apiProject *ApiBambooPlan) *BambooPlan {
-	b := &BambooPlan{
-		PlanKey:                   apiProject.Key,
-		Name:                      apiProject.Name,
-		Expand:                    apiProject.Expand,
-		ProjectKey:                apiProject.ProjectKey,
-		ProjectName:               apiProject.ProjectName,
-		Description:               apiProject.Description,
-		ShortName:                 apiProject.ShortName,
-		BuildName:                 apiProject.BuildName,
-		ShortKey:                  apiProject.ShortKey,
-		Type:                      apiProject.Type,
-		Enabled:                   apiProject.Enabled,
-		Href:                      apiProject.Href,
-		Rel:                       apiProject.Rel,
-		IsFavourite:               apiProject.IsFavourite,
-		IsActive:                  apiProject.IsActive,
-		IsBuilding:                apiProject.IsBuilding,
-		AverageBuildTimeInSeconds: apiProject.AverageBuildTimeInSeconds,
-	}
-	return b
+func (b *BambooPlan) Convert(apiProject *ApiBambooPlan) {
+	b.PlanKey = apiProject.Key
+	b.Name = apiProject.Name
+	b.Expand = apiProject.Expand
+	b.ProjectKey = apiProject.ProjectKey
+	b.ProjectName = apiProject.ProjectName
+	b.Description = apiProject.Description
+	b.ShortName = apiProject.ShortName
+	b.BuildName = apiProject.BuildName
+	b.ShortKey = apiProject.ShortKey
+	b.Type = apiProject.Type
+	b.Enabled = apiProject.Enabled
+	b.Href = apiProject.Href
+	b.Rel = apiProject.Rel
+	b.IsFavourite = apiProject.IsFavourite
+	b.IsActive = apiProject.IsActive
+	b.IsBuilding = apiProject.IsBuilding
+	b.AverageBuildTimeInSeconds = apiProject.AverageBuildTimeInSeconds
 }
 
 func (BambooPlan) TableName() string {
diff --git a/backend/plugins/bamboo/models/project.go b/backend/plugins/bamboo/models/project.go
index d8b903351..89e7caa4b 100644
--- a/backend/plugins/bamboo/models/project.go
+++ b/backend/plugins/bamboo/models/project.go
@@ -19,6 +19,7 @@ package models
 
 import (
 	"encoding/json"
+
 	"github.com/apache/incubator-devlake/core/models/common"
 )
 
@@ -70,3 +71,21 @@ type ApiBambooProjectResponse struct {
 	Link     ApiBambooLink     `json:"link"`
 	Projects ApiBambooProjects `json:"projects"`
 }
+
+type ApiSearchEntityProject struct {
+	Id          string `json:"id"`
+	Key         string `json:"key"`
+	ProjectName string `json:"projectName"`
+	Description string `json:"description"`
+}
+
+type ApiSearchResultProjects struct {
+	Id           string                 `json:"id"`
+	Type         string                 `json:"type"`
+	SearchEntity ApiSearchEntityProject `json:"searchResults"`
+}
+
+type ApiBambooSearchProjectResponse struct {
+	ApiBambooSizeData `json:"squash"`
+	SearchResults     []ApiSearchResultProjects `json:"searchResults"`
+}
diff --git a/backend/plugins/bamboo/tasks/job_extractor.go b/backend/plugins/bamboo/tasks/job_extractor.go
index 74515fcbf..5668eabeb 100644
--- a/backend/plugins/bamboo/tasks/job_extractor.go
+++ b/backend/plugins/bamboo/tasks/job_extractor.go
@@ -19,6 +19,7 @@ package tasks
 
 import (
 	"encoding/json"
+
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -44,7 +45,8 @@ func ExtractJob(taskCtx plugin.SubTaskContext) errors.Error {
 			if err != nil {
 				return nil, err
 			}
-			body := models.BambooJob{}.Convert(res)
+			body := &models.BambooJob{}
+			body.Convert(res)
 			body.ConnectionId = data.Options.ConnectionId
 			body.ProjectKey = data.Options.ProjectKey
 			body.PlanKey = plan.PlanKey
diff --git a/backend/plugins/bamboo/tasks/plan_extractor.go b/backend/plugins/bamboo/tasks/plan_extractor.go
index 5174b7b5e..43d92e0f1 100644
--- a/backend/plugins/bamboo/tasks/plan_extractor.go
+++ b/backend/plugins/bamboo/tasks/plan_extractor.go
@@ -19,6 +19,7 @@ package tasks
 
 import (
 	"encoding/json"
+
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -39,7 +40,8 @@ func ExtractPlan(taskCtx plugin.SubTaskContext) errors.Error {
 			if err != nil {
 				return nil, err
 			}
-			body := models.BambooPlan{}.Convert(res)
+			body := &models.BambooPlan{}
+			body.Convert(res)
 			body.ConnectionId = data.Options.ConnectionId
 			return []interface{}{body}, nil
 		},
diff --git a/backend/plugins/sonarqube/api/scope.go b/backend/plugins/sonarqube/api/scope.go
index 063020133..3ec71af35 100644
--- a/backend/plugins/sonarqube/api/scope.go
+++ b/backend/plugins/sonarqube/api/scope.go
@@ -116,7 +116,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Description get Sonarqube projects
 // @Tags plugins/sonarqube
 // @Param connectionId path int false "connection ID"
-// @Success 200  {object} []apiProject
+// @Success 200  {object} []models.SonarqubeProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/ [GET]
@@ -143,7 +143,7 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Param projectKey path string false "project key"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
-// @Success 200  {object} apiProject
+// @Success 200  {object} models.SonarqubeProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{projectKey} [GET]