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/20 05:36:58 UTC

[incubator-devlake] branch main updated: Bamboo blueprint (#4446)

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 b133b1e38 Bamboo blueprint (#4446)
b133b1e38 is described below

commit b133b1e38d60006b850ac0ed7be4d28aaa294c5f
Author: mappjzc <zh...@merico.dev>
AuthorDate: Mon Feb 20 13:36:53 2023 +0800

    Bamboo blueprint (#4446)
    
    * feat: bamboo blueprint
    
    Add bamboo blueprint.
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix for merge
    
    fix for merge
    
    Nddtfjiang <zh...@merico.dev>
---
 backend/plugins/bamboo/api/blueprint_V200_test.go  | 163 ++++++++++++++++++
 backend/plugins/bamboo/api/blueprint_v200.go       | 190 +++++++++++++++++++++
 backend/plugins/bamboo/impl/impl.go                |  46 ++++-
 backend/plugins/bamboo/models/project.go           |  47 ++++-
 .../plugins/bamboo/models/{project.go => share.go} |  20 +--
 .../models/{project.go => transformation_rule.go}  |  20 +--
 backend/plugins/bamboo/tasks/project_extractor.go  |  19 +--
 backend/plugins/bamboo/tasks/projects_convertor.go |   3 +-
 backend/plugins/bamboo/tasks/shared.go             |   3 +-
 backend/plugins/bamboo/tasks/task_data.go          |   4 +
 10 files changed, 459 insertions(+), 56 deletions(-)

diff --git a/backend/plugins/bamboo/api/blueprint_V200_test.go b/backend/plugins/bamboo/api/blueprint_V200_test.go
new file mode 100644
index 000000000..3622b93c2
--- /dev/null
+++ b/backend/plugins/bamboo/api/blueprint_V200_test.go
@@ -0,0 +1,163 @@
+/*
+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 (
+	"testing"
+
+	mockdal "github.com/apache/incubator-devlake/mocks/core/dal"
+	mockplugin "github.com/apache/incubator-devlake/mocks/core/plugin"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/models/common"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/devops"
+	"github.com/apache/incubator-devlake/core/plugin"
+	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/go-playground/validator/v10"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestMakeDataSourcePipelinePlanV200(t *testing.T) {
+	const testConnectionID uint64 = 1
+	const testTransformationRuleId uint64 = 2
+	const testKey string = "TEST"
+	const testBambooEndPoint string = "http://mail.nddtf.com:8085/rest/api/latest/"
+	const testLink string = "http://mail.nddtf.com:8085/rest/api/latest/project/TEST"
+	const testUser string = "username"
+	const testPass string = "password"
+	const testName string = "bamboo-test"
+	const testTransformationRuleName string = "bamboo transformation rule"
+	const testProxy string = ""
+
+	syncPolicy := &plugin.BlueprintSyncPolicy{}
+	bpScopes := []*plugin.BlueprintScopeV200{
+		{
+			Entities: []string{plugin.DOMAIN_TYPE_CICD},
+			Id:       testKey,
+			Name:     testName,
+		},
+	}
+
+	var testBambooProject = &models.BambooProject{
+		ConnectionId: testConnectionID,
+		ProjectKey:   testKey,
+		Name:         testName,
+		Href:         testLink,
+
+		TransformationRuleId: testTransformationRuleId,
+	}
+
+	var testTransformationRule = &models.BambooTransformationRule{
+		Model: common.Model{
+			ID: testTransformationRuleId,
+		},
+		Name: testTransformationRuleName,
+	}
+
+	var testBambooConnection = &models.BambooConnection{
+		BaseConnection: helper.BaseConnection{
+			Name: testName,
+			Model: common.Model{
+				ID: testConnectionID,
+			},
+		},
+		BambooConn: models.BambooConn{
+			RestConnection: helper.RestConnection{
+				Endpoint:         testBambooEndPoint,
+				Proxy:            testProxy,
+				RateLimitPerHour: 0,
+			},
+			BasicAuth: helper.BasicAuth{
+				Username: testUser,
+				Password: testPass,
+			},
+		},
+	}
+
+	var expectRepoId = "bamboo:BambooProject:1:TEST"
+
+	var testSubTaskMeta = []plugin.SubTaskMeta{}
+
+	var expectPlans = plugin.PipelinePlan{
+		{
+			{
+				Plugin:   "bamboo",
+				Subtasks: []string{},
+				Options: map[string]interface{}{
+					"connectionId":         uint64(1),
+					"projectKey":           testKey,
+					"transformationRuleId": testTransformationRuleId,
+				},
+			},
+		},
+	}
+
+	expectCicdScope := devops.NewCicdScope(expectRepoId, testName)
+	expectCicdScope.Description = ""
+	expectCicdScope.Url = ""
+
+	var err errors.Error
+
+	// register bamboo plugin for NewDomainIdGenerator
+	mockMeta := mockplugin.NewPluginMeta(t)
+	mockMeta.On("RootPkgPath").Return("github.com/apache/incubator-devlake/plugins/bamboo")
+	err = plugin.RegisterPlugin("bamboo", mockMeta)
+	assert.Equal(t, err, nil)
+
+	// Refresh Global Variables and set the sql mock
+	basicRes = unithelper.DummyBasicRes(func(mockDal *mockdal.Dal) {
+		mockDal.On("First", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+			dst := args.Get(0).(*models.BambooConnection)
+			*dst = *testBambooConnection
+		}).Return(nil).Once()
+
+		mockDal.On("First", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+			dst := args.Get(0).(*models.BambooProject)
+			*dst = *testBambooProject
+		}).Return(nil).Twice()
+
+		mockDal.On("First", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+			dst := args.Get(0).(*models.BambooTransformationRule)
+			*dst = *testTransformationRule
+		}).Return(nil).Once()
+	})
+	connectionHelper = helper.NewConnectionHelper(
+		basicRes,
+		validator.New(),
+	)
+
+	plans, scopes, err := MakePipelinePlanV200(testSubTaskMeta, testConnectionID, bpScopes, syncPolicy)
+	assert.Equal(t, err, nil)
+
+	assert.Equal(t, expectPlans, plans)
+
+	// ignore CreatedDate UpdatedDate  CreatedAt UpdatedAt checking
+	expectCicdScope.CreatedDate = scopes[0].(*devops.CicdScope).CreatedDate
+	expectCicdScope.UpdatedDate = scopes[0].(*devops.CicdScope).UpdatedDate
+	expectCicdScope.CreatedAt = scopes[0].(*devops.CicdScope).CreatedAt
+	expectCicdScope.UpdatedAt = scopes[0].(*devops.CicdScope).UpdatedAt
+
+	var expectScopes = []plugin.Scope{
+		expectCicdScope,
+	}
+
+	assert.Equal(t, expectScopes, scopes)
+}
diff --git a/backend/plugins/bamboo/api/blueprint_v200.go b/backend/plugins/bamboo/api/blueprint_v200.go
new file mode 100644
index 000000000..1ce6d404f
--- /dev/null
+++ b/backend/plugins/bamboo/api/blueprint_v200.go
@@ -0,0 +1,190 @@
+/*
+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 (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/plugins/bamboo/models"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/utils"
+
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/devops"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+	plugin "github.com/apache/incubator-devlake/core/plugin"
+	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	aha "github.com/apache/incubator-devlake/helpers/pluginhelper/api/apihelperabstract"
+)
+
+func MakePipelinePlanV200(
+	subtaskMetas []plugin.SubTaskMeta,
+	connectionId uint64,
+	scope []*plugin.BlueprintScopeV200,
+	syncPolicy *plugin.BlueprintSyncPolicy,
+) (plugin.PipelinePlan, []plugin.Scope, errors.Error) {
+	var err errors.Error
+	connection := new(models.BambooConnection)
+	err1 := connectionHelper.FirstById(connection, connectionId)
+	if err1 != nil {
+		return nil, nil, errors.Default.Wrap(err1, fmt.Sprintf("error on get connection by id[%d]", connectionId))
+	}
+
+	sc, err := makeScopeV200(connectionId, scope)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pp, err := makePipelinePlanV200(subtaskMetas, scope, connection, syncPolicy)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return pp, sc, nil
+}
+
+func makeScopeV200(connectionId uint64, scopes []*plugin.BlueprintScopeV200) ([]plugin.Scope, errors.Error) {
+	sc := make([]plugin.Scope, 0, len(scopes))
+
+	for _, scope := range scopes {
+		id := didgen.NewDomainIdGenerator(&models.BambooProject{}).Generate(connectionId, scope.Id)
+
+		// get project from db
+		BambooProject, err := GetprojectByConnectionIdAndscopeId(connectionId, scope.Id)
+		if err != nil {
+			return nil, err
+		}
+
+		// add cicd_scope to scopes
+		if utils.StringsContains(scope.Entities, plugin.DOMAIN_TYPE_CICD) {
+			scopeCICD := devops.NewCicdScope(id, BambooProject.Name)
+
+			sc = append(sc, scopeCICD)
+		}
+	}
+
+	return sc, nil
+}
+
+func makePipelinePlanV200(
+	subtaskMetas []plugin.SubTaskMeta,
+	scopes []*plugin.BlueprintScopeV200,
+	connection *models.BambooConnection, syncPolicy *plugin.BlueprintSyncPolicy,
+) (plugin.PipelinePlan, errors.Error) {
+	plans := make(plugin.PipelinePlan, 0, len(scopes))
+	for _, scope := range scopes {
+		var stage plugin.PipelineStage
+		var err errors.Error
+		// get project
+		project, err := GetprojectByConnectionIdAndscopeId(connection.ID, scope.Id)
+		if err != nil {
+			return nil, err
+		}
+
+		// get transformationRuleId
+		transformationRules, err := GetTransformationRuleByproject(project)
+		if err != nil {
+			return nil, err
+		}
+
+		// bamboo main part
+		options := make(map[string]interface{})
+		options["connectionId"] = connection.ID
+		options["projectKey"] = scope.Id
+		options["transformationRuleId"] = transformationRules.ID
+
+		// construct subtasks
+		subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, scope.Entities)
+		if err != nil {
+			return nil, err
+		}
+
+		stage = append(stage, &plugin.PipelineTask{
+			Plugin:   "bamboo",
+			Subtasks: subtasks,
+			Options:  options,
+		})
+
+		plans = append(plans, stage)
+	}
+	return plans, nil
+}
+
+// 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()
+	err := db.First(project, dal.Where("connection_id = ? AND key = ?", connectionId, key))
+	if err != nil {
+		if db.IsErrorNotFound(err) {
+			return nil, errors.Default.Wrap(err, fmt.Sprintf("can not find project by connection [%d] scope [%s]", connectionId, scopeId))
+		}
+		return nil, errors.Default.Wrap(err, fmt.Sprintf("fail to find project by connection [%d] scope [%s]", connectionId, scopeId))
+	}
+
+	return project, nil
+}
+
+// GetTransformationRuleByproject get the GetTransformationRule by project
+func GetTransformationRuleByproject(project *models.BambooProject) (*models.BambooTransformationRule, errors.Error) {
+	transformationRules := &models.BambooTransformationRule{}
+	transformationRuleId := project.TransformationRuleId
+	if transformationRuleId != 0 {
+		db := basicRes.GetDal()
+		err := db.First(transformationRules, dal.Where("id = ?", transformationRuleId))
+		if err != nil {
+			if db.IsErrorNotFound(err) {
+				return nil, errors.Default.Wrap(err, fmt.Sprintf("can not find transformationRules by transformationRuleId [%d]", transformationRuleId))
+			}
+			return nil, errors.Default.Wrap(err, fmt.Sprintf("fail to find transformationRules by transformationRuleId [%d]", transformationRuleId))
+		}
+	} else {
+		transformationRules.ID = 0
+	}
+
+	return transformationRules, nil
+}
+
+func GetApiProject(
+	projectKey string,
+	apiClient aha.ApiClientAbstract,
+) (*models.ApiBambooProject, errors.Error) {
+	projectRes := &models.ApiBambooProject{}
+	res, err := apiClient.Get(fmt.Sprintf("project/%s.json", projectKey), nil, nil)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	if res.StatusCode != http.StatusOK {
+		return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code when requesting project detail from %s", res.Request.URL.String()))
+	}
+	body, err := errors.Convert01(io.ReadAll(res.Body))
+	if err != nil {
+		return nil, err
+	}
+	err = errors.Convert(json.Unmarshal(body, projectRes))
+	if err != nil {
+		return nil, err
+	}
+	return projectRes, nil
+}
diff --git a/backend/plugins/bamboo/impl/impl.go b/backend/plugins/bamboo/impl/impl.go
index f8347c0b7..2680319c0 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -64,8 +64,7 @@ func (p Bamboo) TransformationRule() interface{} {
 }
 
 func (p Bamboo) MakeDataSourcePipelinePlanV200(connectionId uint64, scopes []*plugin.BlueprintScopeV200, syncPolicy plugin.BlueprintSyncPolicy) (plugin.PipelinePlan, []plugin.Scope, errors.Error) {
-	//return api.MakePipelinePlanV200(p.SubTaskMetas(), connectionId, scopes, &syncPolicy)
-	return nil, nil, nil
+	return api.MakePipelinePlanV200(p.SubTaskMetas(), connectionId, scopes, &syncPolicy)
 }
 
 func (p Bamboo) GetTablesInfo() []dal.Tabler {
@@ -89,6 +88,9 @@ func (p Bamboo) SubTaskMetas() []plugin.SubTaskMeta {
 }
 
 func (p Bamboo) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]interface{}) (interface{}, errors.Error) {
+	logger := taskCtx.GetLogger()
+	logger.Debug("%v", options)
+
 	op, err := tasks.DecodeAndValidateTaskOptions(options)
 	if err != nil {
 		return nil, err
@@ -108,6 +110,43 @@ func (p Bamboo) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]i
 		return nil, errors.Default.Wrap(err, "unable to get Bamboo API client instance")
 	}
 
+	if op.ProjectKey != "" {
+		var scope *models.BambooProject
+		// support v100 & advance mode
+		// If we still cannot find the record in db, we have to request from remote server and save it to db
+		db := taskCtx.GetDal()
+		err = db.First(&scope, dal.Where("connection_id = ? AND key = ?", op.ConnectionId, op.ProjectKey))
+		if err != nil && db.IsErrorNotFound(err) {
+			apiProject, err := api.GetApiProject(op.ProjectKey, apiClient)
+			if err != nil {
+				return nil, err
+			}
+			logger.Debug(fmt.Sprintf("Current project: %s", apiProject.Key))
+			scope.Convert(apiProject)
+			scope.ConnectionId = op.ConnectionId
+			err = taskCtx.GetDal().CreateIfNotExist(&scope)
+			if err != nil {
+				return nil, err
+			}
+		}
+		if err != nil {
+			return nil, errors.Default.Wrap(err, fmt.Sprintf("fail to find project: %s", op.ProjectKey))
+		}
+	}
+
+	if op.BambooTransformationRule == nil && op.TransformationRuleId != 0 {
+		var transformationRule models.BambooTransformationRule
+		db := taskCtx.GetDal()
+		err = db.First(&transformationRule, dal.Where("id = ?", op.TransformationRuleId))
+		if err != nil {
+			if db.IsErrorNotFound(err) {
+				return nil, errors.Default.Wrap(err, fmt.Sprintf("can not find transformationRules by transformationRuleId [%d]", op.TransformationRuleId))
+			}
+			return nil, errors.Default.Wrap(err, fmt.Sprintf("fail to find transformationRules by transformationRuleId [%d]", op.TransformationRuleId))
+		}
+		op.BambooTransformationRule = &transformationRule
+	}
+
 	return &tasks.BambooTaskData{
 		Options:   op,
 		ApiClient: apiClient,
@@ -141,8 +180,7 @@ func (p Bamboo) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 }
 
 func (p Bamboo) MakePipelinePlan(connectionId uint64, scope []*plugin.BlueprintScopeV100) (plugin.PipelinePlan, errors.Error) {
-	//return api.MakePipelinePlan(p.SubTaskMetas(), connectionId, scope)
-	return nil, nil
+	return nil, errors.Default.New("Bamboo don't support blueprint v100")
 }
 
 func (p Bamboo) Close(taskCtx plugin.TaskContext) errors.Error {
diff --git a/backend/plugins/bamboo/models/project.go b/backend/plugins/bamboo/models/project.go
index 7da7ba561..c621aa8bc 100644
--- a/backend/plugins/bamboo/models/project.go
+++ b/backend/plugins/bamboo/models/project.go
@@ -19,17 +19,46 @@ package models
 
 import "github.com/apache/incubator-devlake/core/models/common"
 
+type ApiBambooProject struct {
+	Key         string            `json:"key"`
+	Expand      string            `json:"expand"`
+	Name        string            `json:"name"`
+	Description string            `json:"description"`
+	Link        ApiBambooLink     `json:"link"`
+	Plans       ApiBambooSizeData `json:"plans"`
+}
+
+type ApiBambooProjects struct {
+	ApiBambooSizeData
+	Expand   string             `json:"expand"`
+	Link     ApiBambooLink      `json:"link"`
+	Projects []ApiBambooProject `json:"project"`
+}
+
+type ApiBambooProjectResponse struct {
+	Expand   string            `json:"expand"`
+	Link     ApiBambooLink     `json:"link"`
+	Projects ApiBambooProjects `json:"projects"`
+}
+
 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)"`
-	common.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)"`
+	common.NoPKModel     `json:"-" mapstructure:"-"`
+}
+
+func (b *BambooProject) Convert(apiProject *ApiBambooProject) {
+	b.ProjectKey = apiProject.Key
+	b.Name = apiProject.Name
+	b.Description = apiProject.Description
+	b.Href = apiProject.Link.Href
 }
 
-func (BambooProject) TableName() string {
+func (b *BambooProject) TableName() string {
 	return "_tool_bamboo_projects"
 }
diff --git a/backend/plugins/bamboo/models/project.go b/backend/plugins/bamboo/models/share.go
similarity index 60%
copy from backend/plugins/bamboo/models/project.go
copy to backend/plugins/bamboo/models/share.go
index 7da7ba561..77ab6c2e6 100644
--- a/backend/plugins/bamboo/models/project.go
+++ b/backend/plugins/bamboo/models/share.go
@@ -17,19 +17,13 @@ limitations under the License.
 
 package models
 
-import "github.com/apache/incubator-devlake/core/models/common"
-
-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)"`
-	common.NoPKModel
+type ApiBambooLink struct {
+	Href string `json:"href"`
+	Rel  string `json:"rel"`
 }
 
-func (BambooProject) TableName() string {
-	return "_tool_bamboo_projects"
+type ApiBambooSizeData struct {
+	Size       int `json:"size"`
+	StartIndex int `json:"start-index"`
+	MaxResult  int `json:"max-result"`
 }
diff --git a/backend/plugins/bamboo/models/project.go b/backend/plugins/bamboo/models/transformation_rule.go
similarity index 61%
copy from backend/plugins/bamboo/models/project.go
copy to backend/plugins/bamboo/models/transformation_rule.go
index 7da7ba561..4d48d5f19 100644
--- a/backend/plugins/bamboo/models/project.go
+++ b/backend/plugins/bamboo/models/transformation_rule.go
@@ -17,19 +17,15 @@ limitations under the License.
 
 package models
 
-import "github.com/apache/incubator-devlake/core/models/common"
+import (
+	"github.com/apache/incubator-devlake/core/models/common"
+)
 
-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)"`
-	common.NoPKModel
+type BambooTransformationRule struct {
+	common.Model
+	Name string `gorm:"type:varchar(255);index:idx_name_gitlab,unique" validate:"required" mapstructure:"name" json:"name"`
 }
 
-func (BambooProject) TableName() string {
-	return "_tool_bamboo_projects"
+func (t BambooTransformationRule) TableName() string {
+	return "_tool_bamboo_transformation_rules"
 }
diff --git a/backend/plugins/bamboo/tasks/project_extractor.go b/backend/plugins/bamboo/tasks/project_extractor.go
index 7263ea14e..4423a6402 100644
--- a/backend/plugins/bamboo/tasks/project_extractor.go
+++ b/backend/plugins/bamboo/tasks/project_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"
@@ -34,7 +35,7 @@ func ExtractProject(taskCtx plugin.SubTaskContext) errors.Error {
 		RawDataSubTaskArgs: *rawDataSubTaskArgs,
 
 		Extract: func(resData *helper.RawData) ([]interface{}, errors.Error) {
-			res := &ApiProject{}
+			res := &models.ApiBambooProject{}
 			err := errors.Convert(json.Unmarshal(resData.Data, res))
 			if err != nil {
 				return nil, err
@@ -60,9 +61,8 @@ var ExtractProjectMeta = plugin.SubTaskMeta{
 }
 
 // Convert the API response to our DB model instance
-func ConvertProject(bambooApiProject *ApiProject) *models.BambooProject {
+func ConvertProject(bambooApiProject *models.ApiBambooProject) *models.BambooProject {
 	bambooProject := &models.BambooProject{
-		Expand:      bambooApiProject.Expand,
 		ProjectKey:  bambooApiProject.Key,
 		Name:        bambooApiProject.Name,
 		Description: bambooApiProject.Description,
@@ -72,19 +72,6 @@ func ConvertProject(bambooApiProject *ApiProject) *models.BambooProject {
 	return bambooProject
 }
 
-type ApiProject struct {
-	Expand      string  `json:"expand"`
-	Key         string  `json:"key"`
-	Name        string  `json:"name"`
-	Description string  `json:"description"`
-	Link        ApiLink `json:"link"`
-	//Plans       []ApiPlan `json:"plans"`
-}
-type ApiLink struct {
-	Href string `json:"href"`
-	Rel  string `json:"rel"`
-}
-
 //type ApiProject struct {
 //	ProjectKey         string `json:"key"`
 //	Name        string `json:"name"`
diff --git a/backend/plugins/bamboo/tasks/projects_convertor.go b/backend/plugins/bamboo/tasks/projects_convertor.go
index 3de31efda..0db91aad3 100644
--- a/backend/plugins/bamboo/tasks/projects_convertor.go
+++ b/backend/plugins/bamboo/tasks/projects_convertor.go
@@ -18,9 +18,10 @@ limitations under the License.
 package tasks
 
 import (
-	"github.com/apache/incubator-devlake/core/models/domainlayer/devops"
 	"reflect"
 
+	"github.com/apache/incubator-devlake/core/models/domainlayer/devops"
+
 	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/models/domainlayer"
diff --git a/backend/plugins/bamboo/tasks/shared.go b/backend/plugins/bamboo/tasks/shared.go
index 97f96278a..3e89bc80d 100644
--- a/backend/plugins/bamboo/tasks/shared.go
+++ b/backend/plugins/bamboo/tasks/shared.go
@@ -18,10 +18,11 @@ limitations under the License.
 package tasks
 
 import (
+	"net/http"
+
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-	"net/http"
 )
 
 func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string) (*api.RawDataSubTaskArgs, *BambooTaskData) {
diff --git a/backend/plugins/bamboo/tasks/task_data.go b/backend/plugins/bamboo/tasks/task_data.go
index 48528e9e9..5e2fe4392 100644
--- a/backend/plugins/bamboo/tasks/task_data.go
+++ b/backend/plugins/bamboo/tasks/task_data.go
@@ -20,6 +20,7 @@ package tasks
 import (
 	"github.com/apache/incubator-devlake/core/errors"
 	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/bamboo/models"
 )
 
 type BambooApiParams struct {
@@ -36,6 +37,9 @@ type BambooOptions struct {
 	ProjectKey       string   `json:"projectKey"`
 	CreatedDateAfter string   `json:"createdDateAfter" mapstructure:"createdDateAfter,omitempty"`
 	Tasks            []string `json:"tasks,omitempty"`
+
+	TransformationRuleId             uint64 `mapstructure:"transformationRuleId" json:"transformationRuleId"`
+	*models.BambooTransformationRule `mapstructure:"transformationRules" json:"transformationRules"`
 }
 
 type BambooTaskData struct {