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

warren pushed a commit to branch feat-plugin-zentao
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git

commit e75bcd36c00a5d3736a13e400860b49065207038
Author: Yingchu Chen <[email protected]>
AuthorDate: Tue Sep 6 18:03:04 2022 +0800

    feat(zentao): create new plugin
    
    Relate to #2961
---
 plugins/zentao/api/blueprint.go                    |  69 ++++++++++
 plugins/zentao/api/connection.go                   | 149 +++++++++++++++++++++
 plugins/zentao/api/init.go                         |  39 ++++++
 plugins/zentao/impl/impl.go                        | 124 +++++++++++++++++
 plugins/zentao/models/archived/connection.go       |  70 ++++++++++
 plugins/zentao/models/connection.go                |  51 +++++++
 .../migrationscripts/20220906_add_init_tables.go   |  39 ++++++
 plugins/zentao/models/migrationscripts/register.go |  27 ++++
 plugins/zentao/tasks/api_client.go                 |  92 +++++++++++++
 plugins/zentao/tasks/project_collector.go          |  78 +++++++++++
 plugins/zentao/tasks/task_data.go                  |  61 +++++++++
 plugins/zentao/zentao.go                           |  47 +++++++
 12 files changed, 846 insertions(+)

diff --git a/plugins/zentao/api/blueprint.go b/plugins/zentao/api/blueprint.go
new file mode 100644
index 00000000..3fa0e25f
--- /dev/null
+++ b/plugins/zentao/api/blueprint.go
@@ -0,0 +1,69 @@
+/*
+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"
+
+       "github.com/apache/incubator-devlake/plugins/core"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func MakePipelinePlan(subtaskMetas []core.SubTaskMeta, connectionId uint64, 
scope []*core.BlueprintScopeV100) (core.PipelinePlan, error) {
+       var err error
+       plan := make(core.PipelinePlan, len(scope))
+       for i, scopeElem := range scope {
+               taskOptions := make(map[string]interface{})
+               err = json.Unmarshal(scopeElem.Options, &taskOptions)
+               if err != nil {
+                       return nil, err
+               }
+               taskOptions["connectionId"] = connectionId
+
+               //TODO Add transformation rules to task options
+
+        /*
+        var transformationRules tasks.TransformationRules
+        if len(scopeElem.Transformation) > 0 {
+            err = json.Unmarshal(scopeElem.Transformation, 
&transformationRules)
+            if err != nil {
+                return nil, err
+            }
+        }
+        */
+               //taskOptions["transformationRules"] = transformationRules
+               _, err := tasks.DecodeAndValidateTaskOptions(taskOptions)
+               if err != nil {
+                       return nil, err
+               }
+               // subtasks
+               subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, 
scopeElem.Entities)
+               if err != nil {
+                       return nil, err
+               }
+               plan[i] = core.PipelineStage{
+                       {
+                               Plugin:   "zentao",
+                               Subtasks: subtasks,
+                               Options:  taskOptions,
+                       },
+               }
+       }
+       return plan, nil
+}
diff --git a/plugins/zentao/api/connection.go b/plugins/zentao/api/connection.go
new file mode 100644
index 00000000..2f8183d5
--- /dev/null
+++ b/plugins/zentao/api/connection.go
@@ -0,0 +1,149 @@
+/*
+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"
+       "github.com/apache/incubator-devlake/errors"
+       "github.com/apache/incubator-devlake/plugins/core"
+       "github.com/apache/incubator-devlake/plugins/feishu/apimodels"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/apache/incubator-devlake/plugins/zentao/models"
+       "github.com/mitchellh/mapstructure"
+       "net/http"
+)
+
+//TODO Please modify the following code to fit your needs
+func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, 
error) {
+       // process input
+       var params models.TestConnectionRequest
+       err := mapstructure.Decode(input.Body, &params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "could not decode request 
parameters", errors.AsUserMessage())
+       }
+       err = vld.Struct(params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "could not validate 
request parameters", errors.AsUserMessage())
+       }
+
+       authApiClient, err := helper.NewApiClient(context.TODO(), 
params.Endpoint, nil, 0, params.Proxy, basicRes)
+       if err != nil {
+               return nil, err
+       }
+
+       // request for access token
+       tokenReqBody := &apimodels.ApiAccessTokenRequest{
+               AppId:     params.Username,
+               AppSecret: params.Password,
+       }
+       tokenRes, err := authApiClient.Post("/tokens", nil, tokenReqBody, nil)
+       if err != nil {
+               return nil, err
+       }
+       tokenResBody := &apimodels.ApiAccessTokenResponse{}
+       err = helper.UnmarshalResponse(tokenRes, tokenResBody)
+       if err != nil {
+               return nil, err
+       }
+       if tokenResBody.AppAccessToken == "" && tokenResBody.TenantAccessToken 
== "" {
+               return nil, errors.Default.New("failed to request access token")
+       }
+
+       // output
+       return nil, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+POST /plugins/Zentao/connections
+{
+       "name": "Zentao data connection name",
+       "endpoint": "Zentao api endpoint, i.e. https://example.com";,
+       "username": "username, usually should be email address",
+       "password": "Zentao api access token"
+}
+*/
+func PostConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, 
error) {
+       // update from request and save to database
+       connection := &models.ZentaoConnection{}
+       err := connectionHelper.Create(connection, input)
+       if err != nil {
+               return nil, err
+       }
+       return &core.ApiResourceOutput{Body: connection, Status: 
http.StatusOK}, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+PATCH /plugins/Zentao/connections/:connectionId
+{
+       "name": "Zentao data connection name",
+       "endpoint": "Zentao api endpoint, i.e. https://example.com";,
+       "username": "username, usually should be email address",
+       "password": "Zentao api access token"
+}
+*/
+func PatchConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, 
error) {
+       connection := &models.ZentaoConnection{}
+       err := connectionHelper.Patch(connection, input)
+       if err != nil {
+               return nil, err
+       }
+       return &core.ApiResourceOutput{Body: connection}, nil
+}
+
+/*
+DELETE /plugins/Zentao/connections/:connectionId
+*/
+func DeleteConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, 
error) {
+       connection := &models.ZentaoConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, err
+       }
+       err = connectionHelper.Delete(connection)
+       return &core.ApiResourceOutput{Body: connection}, err
+}
+
+/*
+GET /plugins/Zentao/connections
+*/
+func ListConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, 
error) {
+       var connections []models.ZentaoConnection
+       err := connectionHelper.List(&connections)
+       if err != nil {
+               return nil, err
+       }
+       return &core.ApiResourceOutput{Body: connections, Status: 
http.StatusOK}, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+GET /plugins/Zentao/connections/:connectionId
+{
+       "name": "Zentao data connection name",
+       "endpoint": "Zentao api endpoint, i.e. 
https://merico.atlassian.net/rest";,
+       "username": "username, usually should be email address",
+       "password": "Zentao api access token"
+}
+*/
+func GetConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, 
error) {
+       connection := &models.ZentaoConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       return &core.ApiResourceOutput{Body: connection}, err
+}
diff --git a/plugins/zentao/api/init.go b/plugins/zentao/api/init.go
new file mode 100644
index 00000000..6774e148
--- /dev/null
+++ b/plugins/zentao/api/init.go
@@ -0,0 +1,39 @@
+/*
+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 (
+       "github.com/apache/incubator-devlake/plugins/core"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/go-playground/validator/v10"
+       "github.com/spf13/viper"
+       "gorm.io/gorm"
+)
+
+var vld *validator.Validate
+var connectionHelper *helper.ConnectionApiHelper
+var basicRes core.BasicRes
+
+func Init(config *viper.Viper, logger core.Logger, database *gorm.DB) {
+       basicRes = helper.NewDefaultBasicRes(config, logger, database)
+       vld = validator.New()
+       connectionHelper = helper.NewConnectionHelper(
+               basicRes,
+               vld,
+       )
+}
diff --git a/plugins/zentao/impl/impl.go b/plugins/zentao/impl/impl.go
new file mode 100644
index 00000000..cc81c226
--- /dev/null
+++ b/plugins/zentao/impl/impl.go
@@ -0,0 +1,124 @@
+/*
+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 impl
+
+import (
+       "fmt"
+       "github.com/apache/incubator-devlake/migration"
+       "github.com/apache/incubator-devlake/plugins/core"
+    "github.com/apache/incubator-devlake/plugins/zentao/api"
+    "github.com/apache/incubator-devlake/plugins/zentao/models"
+    
"github.com/apache/incubator-devlake/plugins/zentao/models/migrationscripts"
+       "github.com/apache/incubator-devlake/plugins/zentao/tasks"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/spf13/viper"
+       "gorm.io/gorm"
+)
+
+// make sure interface is implemented
+var _ core.PluginMeta = (*Zentao)(nil)
+var _ core.PluginInit = (*Zentao)(nil)
+var _ core.PluginTask = (*Zentao)(nil)
+var _ core.PluginApi = (*Zentao)(nil)
+var _ core.PluginBlueprintV100 = (*Zentao)(nil)
+var _ core.CloseablePluginTask = (*Zentao)(nil)
+
+
+
+type Zentao struct{}
+
+func (plugin Zentao) Description() string {
+       return "collect some Zentao data"
+}
+
+func (plugin Zentao) Init(config *viper.Viper, logger core.Logger, db 
*gorm.DB) error {
+       api.Init(config, logger, db)
+       return nil
+}
+
+func (plugin Zentao) SubTaskMetas() []core.SubTaskMeta {
+       // TODO add your sub task here
+       return []core.SubTaskMeta{
+               tasks.CollectProjectMeta,
+       }
+}
+
+func (plugin Zentao) PrepareTaskData(taskCtx core.TaskContext, options 
map[string]interface{}) (interface{}, error) {
+       op, err := tasks.DecodeAndValidateTaskOptions(options)
+    if err != nil {
+        return nil, err
+    }
+    connectionHelper := helper.NewConnectionHelper(
+        taskCtx,
+        nil,
+    )
+    connection := &models.ZentaoConnection{}
+    err = connectionHelper.FirstById(connection, op.ConnectionId)
+    if err != nil {
+        return nil, fmt.Errorf("unable to get Zentao connection by the given 
connection ID: %v", err)
+    }
+
+    apiClient, err := tasks.NewZentaoApiClient(taskCtx, connection)
+    if err != nil {
+        return nil, fmt.Errorf("unable to get Zentao API client instance: %v", 
err)
+    }
+
+    return &tasks.ZentaoTaskData{
+        Options:   op,
+        ApiClient: apiClient,
+    }, nil
+}
+
+// PkgPath information lost when compiled as plugin(.so)
+func (plugin Zentao) RootPkgPath() string {
+       return "github.com/apache/incubator-devlake/plugins/zentao"
+}
+
+func (plugin Zentao) MigrationScripts() []migration.Script {
+       return migrationscripts.All()
+}
+
+func (plugin Zentao) ApiResources() 
map[string]map[string]core.ApiResourceHandler {
+    return map[string]map[string]core.ApiResourceHandler{
+        "test": {
+            "POST": api.TestConnection,
+        },
+        "connections": {
+            "POST": api.PostConnections,
+            "GET":  api.ListConnections,
+        },
+        "connections/:connectionId": {
+            "GET":    api.GetConnection,
+            "PATCH":  api.PatchConnection,
+            "DELETE": api.DeleteConnection,
+        },
+    }
+}
+
+func (plugin Zentao) MakePipelinePlan(connectionId uint64, scope 
[]*core.BlueprintScopeV100) (core.PipelinePlan, error) {
+       return api.MakePipelinePlan(plugin.SubTaskMetas(), connectionId, scope)
+}
+
+func (plugin Zentao) Close(taskCtx core.TaskContext) error {
+       data, ok := taskCtx.GetData().(*tasks.ZentaoTaskData)
+       if !ok {
+               return fmt.Errorf("GetData failed when try to close %+v", 
taskCtx)
+       }
+       data.ApiClient.Release()
+       return nil
+}
diff --git a/plugins/zentao/models/archived/connection.go 
b/plugins/zentao/models/archived/connection.go
new file mode 100644
index 00000000..60d7178c
--- /dev/null
+++ b/plugins/zentao/models/archived/connection.go
@@ -0,0 +1,70 @@
+/*
+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 models
+
+import (
+       "github.com/apache/incubator-devlake/models/migrationscripts/archived"
+)
+
+//TODO Please modify the following code to fit your needs
+// This object conforms to what the frontend currently sends.
+type ZentaoConnection struct {
+       RestConnection `mapstructure:",squash"`
+       //TODO you may need to use helper.BasicAuth instead of 
helper.AccessToken
+       BasicAuth `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+       Endpoint  string `json:"endpoint"`
+       Proxy     string `json:"proxy"`
+       BasicAuth `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type ZentaoResponse struct {
+       Name string `json:"name"`
+       ID   int    `json:"id"`
+       ZentaoConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+       Id   int
+       Name string `json:"name"`
+}
+
+func (ZentaoConnection) TableName() string {
+       return "_tool_zentao_connections"
+}
+
+type BasicAuth struct {
+       Username string `mapstructure:"username" validate:"required" 
json:"username"`
+       Password string `mapstructure:"password" validate:"required" 
json:"password"`
+}
+
+type RestConnection struct {
+       BaseConnection   `mapstructure:",squash"`
+       Endpoint         string `mapstructure:"endpoint" validate:"required" 
json:"endpoint"`
+       Proxy            string `mapstructure:"proxy" json:"proxy"`
+       RateLimitPerHour int    `comment:"api request rate limt per hour" 
json:"rateLimit"`
+}
+
+type BaseConnection struct {
+       Name string `gorm:"type:varchar(100);uniqueIndex" json:"name" 
validate:"required"`
+       archived.Model
+}
diff --git a/plugins/zentao/models/connection.go 
b/plugins/zentao/models/connection.go
new file mode 100644
index 00000000..6140d7f4
--- /dev/null
+++ b/plugins/zentao/models/connection.go
@@ -0,0 +1,51 @@
+/*
+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 models
+
+import "github.com/apache/incubator-devlake/plugins/helper"
+
+//TODO Please modify the following code to fit your needs
+// This object conforms to what the frontend currently sends.
+type ZentaoConnection struct {
+       helper.RestConnection `mapstructure:",squash"`
+       //TODO you may need to use helper.BasicAuth instead of 
helper.AccessToken
+       helper.BasicAuth `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+       Endpoint         string `json:"endpoint"`
+       Proxy            string `json:"proxy"`
+       helper.BasicAuth `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type ZentaoResponse struct {
+       Name string `json:"name"`
+       ID   int    `json:"id"`
+       ZentaoConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+       Id   int
+       Name string `json:"name"`
+}
+
+func (ZentaoConnection) TableName() string {
+       return "_tool_zentao_connections"
+}
diff --git a/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go 
b/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go
new file mode 100644
index 00000000..181bbc77
--- /dev/null
+++ b/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go
@@ -0,0 +1,39 @@
+/*
+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 migrationscripts
+
+import (
+       "context"
+       "gorm.io/gorm"
+)
+
+type addInitTables struct {}
+
+func (u *addInitTables) Up(ctx context.Context, db *gorm.DB) error {
+       return db.Migrator().AutoMigrate(
+               // TODO add you models
+       )
+}
+
+func (*addInitTables) Version() uint64 {
+       return 20220906000001
+}
+
+func (*addInitTables) Name() string {
+       return "zentao init schemas"
+}
diff --git a/plugins/zentao/models/migrationscripts/register.go 
b/plugins/zentao/models/migrationscripts/register.go
new file mode 100644
index 00000000..92e20c01
--- /dev/null
+++ b/plugins/zentao/models/migrationscripts/register.go
@@ -0,0 +1,27 @@
+/*
+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 migrationscripts
+
+import "github.com/apache/incubator-devlake/migration"
+
+// All return all the migration scripts
+func All() []migration.Script {
+       return []migration.Script{
+               new(addInitTables),
+       }
+}
diff --git a/plugins/zentao/tasks/api_client.go 
b/plugins/zentao/tasks/api_client.go
new file mode 100644
index 00000000..62d2d954
--- /dev/null
+++ b/plugins/zentao/tasks/api_client.go
@@ -0,0 +1,92 @@
+/*
+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 tasks
+
+import (
+       "fmt"
+       "github.com/apache/incubator-devlake/errors"
+       "github.com/apache/incubator-devlake/plugins/feishu/apimodels"
+       "net/http"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/plugins/core"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+func NewZentaoApiClient(taskCtx core.TaskContext, connection 
*models.ZentaoConnection) (*helper.ApiAsyncClient, error) {
+       authApiClient, err := helper.NewApiClient(taskCtx.GetContext(), 
connection.Endpoint, nil, 0, connection.Proxy, taskCtx)
+       if err != nil {
+               return nil, err
+       }
+
+       // request for access token
+       tokenReqBody := &apimodels.ApiAccessTokenRequest{
+               AppId:     connection.Username,
+               AppSecret: connection.Password,
+       }
+       tokenRes, err := authApiClient.Post("/tokens", nil, tokenReqBody, nil)
+       if err != nil {
+               return nil, err
+       }
+       tokenResBody := &apimodels.ApiAccessTokenResponse{}
+       err = helper.UnmarshalResponse(tokenRes, tokenResBody)
+       if err != nil {
+               return nil, err
+       }
+       if tokenResBody.AppAccessToken == "" && tokenResBody.TenantAccessToken 
== "" {
+               return nil, errors.Default.New("failed to request access token")
+       }
+       // real request apiClient
+       apiClient, err := helper.NewApiClient(taskCtx.GetContext(), 
connection.Endpoint, nil, 0, connection.Proxy, taskCtx)
+       if err != nil {
+               return nil, err
+       }
+       // set token
+       apiClient.SetHeaders(map[string]string{
+               "Token": fmt.Sprintf("%v", tokenResBody.TenantAccessToken),
+       })
+
+       // create rate limit calculator
+       rateLimiter := &helper.ApiRateLimitCalculator{
+               UserRateLimitPerHour: connection.RateLimitPerHour,
+               DynamicRateLimit: func(res *http.Response) (int, time.Duration, 
error) {
+                       rateLimitHeader := res.Header.Get("RateLimit-Limit")
+                       if rateLimitHeader == "" {
+                               // use default
+                               return 0, 0, nil
+                       }
+                       rateLimit, err := strconv.Atoi(rateLimitHeader)
+                       if err != nil {
+                               return 0, 0, fmt.Errorf("failed to parse 
RateLimit-Limit header: %w", err)
+                       }
+                       // seems like {{ .plugin-ame }} rate limit is on minute 
basis
+                       return rateLimit, 1 * time.Minute, nil
+               },
+       }
+       asyncApiClient, err := helper.CreateAsyncApiClient(
+               taskCtx,
+               apiClient,
+               rateLimiter,
+       )
+       if err != nil {
+               return nil, err
+       }
+       return asyncApiClient, nil
+}
diff --git a/plugins/zentao/tasks/project_collector.go 
b/plugins/zentao/tasks/project_collector.go
new file mode 100644
index 00000000..7af9dc2c
--- /dev/null
+++ b/plugins/zentao/tasks/project_collector.go
@@ -0,0 +1,78 @@
+/*
+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 tasks
+
+import (
+       "encoding/json"
+       "fmt"
+       "github.com/apache/incubator-devlake/plugins/core"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "net/http"
+       "net/url"
+)
+
+const RAW_PROJECT_TABLE = "zentao_project"
+
+var _ core.SubTaskEntryPoint = CollectProject
+
+func CollectProject(taskCtx core.SubTaskContext) error {
+       data := taskCtx.GetData().(*ZentaoTaskData)
+       iterator, err := helper.NewDateIterator(365)
+       if err != nil {
+               return err
+       }
+
+       collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+               RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+                       Ctx:    taskCtx,
+                       Params: ZentaoApiParams{},
+                       Table:  RAW_PROJECT_TABLE,
+               },
+               ApiClient:   data.ApiClient,
+               Incremental: false,
+               Input:       iterator,
+               PageSize:    100,
+               // TODO write which api would you want request
+               UrlTemplate: "projects",
+               Query: func(reqData *helper.RequestData) (url.Values, error) {
+                       query := url.Values{}
+                       query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+                       query.Set("limit", fmt.Sprintf("%v", 
reqData.Pager.Size))
+                       return query, nil
+               },
+               ResponseParser: func(res *http.Response) ([]json.RawMessage, 
error) {
+                       var data struct {
+                               Projects []json.RawMessage `json:"data"`
+                       }
+                       err = helper.UnmarshalResponse(res, &data)
+                       return data.Projects, err
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return collector.Execute()
+}
+
+var CollectProjectMeta = core.SubTaskMeta{
+       Name:             "CollectProject",
+       EntryPoint:       CollectProject,
+       EnabledByDefault: true,
+       Description:      "Collect Project data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/task_data.go 
b/plugins/zentao/tasks/task_data.go
new file mode 100644
index 00000000..a2d88f22
--- /dev/null
+++ b/plugins/zentao/tasks/task_data.go
@@ -0,0 +1,61 @@
+/*
+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 tasks
+
+import (
+       "fmt"
+       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/mitchellh/mapstructure"
+)
+
+type ZentaoApiParams struct {
+       ProductId   uint64
+       ExecutionId uint64
+       ProjectId   uint64
+}
+
+type ZentaoOptions struct {
+       // TODO add some custom options here if necessary
+       // options means some custom params required by plugin running.
+       // Such As How many rows do your want
+       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
+       ConnectionId uint64 `json:"connectionId"`
+       ProductId    uint64
+       ExecutionId  uint64
+       ProjectId    uint64
+       Tasks        []string `json:"tasks,omitempty"`
+       Since        string
+}
+
+type ZentaoTaskData struct {
+       Options   *ZentaoOptions
+       ApiClient *helper.ApiAsyncClient
+}
+
+func DecodeAndValidateTaskOptions(options map[string]interface{}) 
(*ZentaoOptions, error) {
+       var op ZentaoOptions
+       err := mapstructure.Decode(options, &op)
+       if err != nil {
+               return nil, err
+       }
+
+       if op.ConnectionId == 0 {
+               return nil, fmt.Errorf("connectionId is invalid")
+       }
+       return &op, nil
+}
diff --git a/plugins/zentao/zentao.go b/plugins/zentao/zentao.go
new file mode 100644
index 00000000..32845c90
--- /dev/null
+++ b/plugins/zentao/zentao.go
@@ -0,0 +1,47 @@
+/*
+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 main
+
+import (
+       "github.com/apache/incubator-devlake/plugins/Zentao/impl"
+       "github.com/apache/incubator-devlake/runner"
+       "github.com/spf13/cobra"
+)
+
+// Export a variable named PluginEntry for Framework to search and load
+var PluginEntry impl.Zentao //nolint
+
+// standalone mode for debugging
+func main() {
+       cmd := &cobra.Command{Use: "zentao"}
+
+       connectionId := cmd.Flags().Uint64P("connectionId", "c", 0, "zentao 
connection id")
+       executionId := cmd.Flags().IntP("executionId", "e", 8, "execution id")
+       productId := cmd.Flags().IntP("productId", "o", 8, "product id")
+       projectId := cmd.Flags().IntP("projectId", "p", 8, "project id")
+
+       cmd.Run = func(cmd *cobra.Command, args []string) {
+               runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
+                       "connectionId": *connectionId,
+                       "executionId":  *executionId,
+                       "productId":    *productId,
+                       "projectId":    *projectId,
+               })
+       }
+       runner.RunCmd(cmd)
+}

Reply via email to