diff --git a/go.mod b/go.mod index a1d3981ef9..e4202c2497 100644 --- a/go.mod +++ b/go.mod @@ -245,6 +245,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/larksuite/project-oapi-sdk-golang v1.0.15 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect diff --git a/go.sum b/go.sum index db2bcd75a4..43aaca900a 100644 --- a/go.sum +++ b/go.sum @@ -742,6 +742,8 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/larksuite/oapi-sdk-go/v3 v3.4.20 h1:Ul1NWAHXYzbXBHFmUxMTSZ9v2ahy/O8EthYOQnLvPo0= github.com/larksuite/oapi-sdk-go/v3 v3.4.20/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= +github.com/larksuite/project-oapi-sdk-golang v1.0.15 h1:5GqWotqVDTWk3Sg+YjTtaocRx/0dHQabU6F2Qd7MUSk= +github.com/larksuite/project-oapi-sdk-golang v1.0.15/go.mod h1:M4gZ6QA4sa6U9iukFsSVQ58LQwlWO8eqk13nArHRHCk= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= diff --git a/pkg/config/config.go b/pkg/config/config.go index ace1f03171..5cc2dc6403 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -295,3 +295,14 @@ func RedisPassword() string { func RedisCommonCacheTokenDB() int { return viper.GetInt(setting.ENVRedisCommonCacheDB) } + +func LarkPluginID() string { + return viper.GetString(setting.ENVLarkPluginID) +} + +func LarkPluginSecret() string { + return viper.GetString(setting.ENVLarkPluginSecret) +} +func LarkPluginAccessTokenType() int { + return viper.GetInt(setting.ENVLarkPluginAccessTokenType) +} \ No newline at end of file diff --git a/pkg/microservice/aslan/core/common/repository/models/build.go b/pkg/microservice/aslan/core/common/repository/models/build.go index aa396d1c16..347b028a2d 100644 --- a/pkg/microservice/aslan/core/common/repository/models/build.go +++ b/pkg/microservice/aslan/core/common/repository/models/build.go @@ -206,6 +206,10 @@ type ServiceWithModule struct { ServiceModule string `bson:"service_module" json:"service_module" yaml:"service_module"` } +func (s *ServiceWithModule) GetKey() string { + return s.ServiceName + "-" + s.ServiceModule +} + type ServiceModuleTargetBase struct { ProductName string `json:"product_name"` ServiceWithModule `json:",inline"` diff --git a/pkg/microservice/aslan/core/common/repository/models/lark_plugin_auth_config.go b/pkg/microservice/aslan/core/common/repository/models/lark_plugin_auth_config.go new file mode 100644 index 0000000000..51741618ad --- /dev/null +++ b/pkg/microservice/aslan/core/common/repository/models/lark_plugin_auth_config.go @@ -0,0 +1,33 @@ +/* +Copyright 2025 The KodeRover Authors. + +Licensed 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 ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type LarkPluginAuthConfig struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + WorkspaceID string `bson:"workspace_id" json:"workspace_id"` + ZadigAddress string `bson:"zadig_address" json:"zadig_address"` + ApiToken string `bson:"api_token" json:"api_token"` + UpdateTime int64 `bson:"update_time" json:"update_time"` +} + +func (LarkPluginAuthConfig) TableName() string { + return "lark_plugin_auth_config" +} diff --git a/pkg/microservice/aslan/core/common/repository/models/lark_plugin_workflow_config.go b/pkg/microservice/aslan/core/common/repository/models/lark_plugin_workflow_config.go new file mode 100644 index 0000000000..39796ff8f3 --- /dev/null +++ b/pkg/microservice/aslan/core/common/repository/models/lark_plugin_workflow_config.go @@ -0,0 +1,42 @@ +/* +Copyright 2025 The KodeRover Authors. + +Licensed 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 ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type LarkPluginWorkflowConfig struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + WorkspaceID string `bson:"workspace_id" json:"workspace_id"` + WorkItemTypeKey string `bson:"work_item_type_key" json:"work_item_type_key"` + Nodes []*LarkPluginWorkflowConfigNode `bson:"nodes" json:"nodes"` + UpdateTime int64 `bson:"update_time" json:"update_time"` +} + +type LarkPluginWorkflowConfigNode struct { + TemplateID int64 `bson:"template_id" json:"template_id"` + TemplateName string `bson:"template_name" json:"template_name"` + NodeID string `bson:"node_id" json:"node_id"` + NodeName string `bson:"node_name" json:"node_name"` + ProjectKey string `bson:"project_key" json:"project_key"` + WorkflowName string `bson:"workflow_name" json:"workflow_name"` +} + +func (LarkPluginWorkflowConfig) TableName() string { + return "lark_plugin_workflow_config" +} diff --git a/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go b/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go index 2de51e466e..1cfffb5d03 100644 --- a/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go +++ b/pkg/microservice/aslan/core/common/repository/models/wokflow_task_v4.go @@ -65,6 +65,9 @@ type WorkflowTask struct { Hash string `bson:"hash" json:"hash"` ApprovalTicketID string `bson:"approval_ticket_id" json:"approval_ticket_id"` ApprovalID string `bson:"approval_id" json:"approval_id"` + + LarkWorkItemTypeKey string `bson:"lark_workitem_type_key" json:"lark_workitem_type_key"` + LarkWorkItemID string `bson:"lark_workitem_id" json:"lark_workitem_id"` } func (WorkflowTask) TableName() string { diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/lark_auth_workflow_config.go b/pkg/microservice/aslan/core/common/repository/mongodb/lark_auth_workflow_config.go new file mode 100644 index 0000000000..df73452244 --- /dev/null +++ b/pkg/microservice/aslan/core/common/repository/mongodb/lark_auth_workflow_config.go @@ -0,0 +1,86 @@ +/* +Copyright 2024 The KodeRover Authors. + +Licensed 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 mongodb + +import ( + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo" +) + +type LarkPluginAuthConfigColl struct { + *mongo.Collection + + coll string +} + +func NewLarkPluginAuthConfigColl() *LarkPluginAuthConfigColl { + name := models.LarkPluginAuthConfig{}.TableName() + return &LarkPluginAuthConfigColl{Collection: mongotool.Database(config.MongoDatabase()).Collection(name), coll: name} +} + +func (c *LarkPluginAuthConfigColl) GetCollectionName() string { + return c.coll +} + +func (c *LarkPluginAuthConfigColl) EnsureIndex(ctx context.Context) error { + mod := []mongo.IndexModel{ + { + Keys: bson.D{ + bson.E{Key: "workspace_id", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + } + + _, err := c.Indexes().CreateMany(ctx, mod) + + return err +} + +func (c *LarkPluginAuthConfigColl) Get(workspaceID string) (*models.LarkPluginAuthConfig, error) { + resp := new(models.LarkPluginAuthConfig) + cursor, err := c.Collection.Find(context.TODO(), bson.M{"workspace_id": workspaceID}) + if err != nil { + return nil, err + } + + err = cursor.All(context.TODO(), &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *LarkPluginAuthConfigColl) Update(config *models.LarkPluginAuthConfig) error { + if config == nil { + return nil + } + + query := bson.M{"workspace_id": config.WorkspaceID} + opts := options.Replace().SetUpsert(true) + _, err := c.ReplaceOne(context.TODO(), query, config, opts) + + return err +} diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/lark_plugin_workflow_config.go b/pkg/microservice/aslan/core/common/repository/mongodb/lark_plugin_workflow_config.go new file mode 100644 index 0000000000..10680fe6bc --- /dev/null +++ b/pkg/microservice/aslan/core/common/repository/mongodb/lark_plugin_workflow_config.go @@ -0,0 +1,123 @@ +/* +Copyright 2024 The KodeRover Authors. + +Licensed 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 mongodb + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo" +) + +type LarkPluginWorkflowConfigColl struct { + *mongo.Collection + + coll string +} + +func NewLarkPluginWorkflowConfigColl() *LarkPluginWorkflowConfigColl { + name := models.LarkPluginWorkflowConfig{}.TableName() + return &LarkPluginWorkflowConfigColl{Collection: mongotool.Database(config.MongoDatabase()).Collection(name), coll: name} +} + +func (c *LarkPluginWorkflowConfigColl) GetCollectionName() string { + return c.coll +} + +func (c *LarkPluginWorkflowConfigColl) EnsureIndex(ctx context.Context) error { + mod := []mongo.IndexModel{ + { + Keys: bson.D{ + bson.E{Key: "workspace_id", Value: 1}, + bson.E{Key: "work_item_type_key", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + } + + _, err := c.Indexes().CreateMany(ctx, mod) + + return err +} + +func (c *LarkPluginWorkflowConfigColl) Get(workspaceID string) ([]*models.LarkPluginWorkflowConfig, error) { + resp := make([]*models.LarkPluginWorkflowConfig, 0) + cursor, err := c.Collection.Find(context.TODO(), bson.M{"workspace_id": workspaceID}) + if err != nil { + return nil, err + } + + err = cursor.All(context.TODO(), &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *LarkPluginWorkflowConfigColl) GetWorkItemTypeConfig(workspaceID, workItemType string) (*models.LarkPluginWorkflowConfig, error) { + resp := new(models.LarkPluginWorkflowConfig) + + query := bson.M{"workspace_id": workspaceID, "work_item_type_key": workItemType} + err := c.Collection.FindOne(context.TODO(), query).Decode(resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (c *LarkPluginWorkflowConfigColl) Update(configs []*models.LarkPluginWorkflowConfig) error { + if len(configs) == 0 { + return nil + } + + // 构建批量操作 + operations := make([]mongo.WriteModel, 0, len(configs)) + for _, config := range configs { + config.UpdateTime = time.Now().Unix() + // 使用 WorkspaceID 和 WorkItemType 作为唯一标识字段进行 upsert 操作 + query := bson.M{"workspace_id": config.WorkspaceID, "work_item_type_key": config.WorkItemTypeKey} + operation := mongo.NewReplaceOneModel(). + SetFilter(query). + SetReplacement(config). + SetUpsert(true) + operations = append(operations, operation) + } + + // 执行批量操作 + opts := options.BulkWrite().SetOrdered(false) + _, err := c.BulkWrite(context.TODO(), operations, opts) + + return err +} + +func (c *LarkPluginWorkflowConfigColl) Delete(workItemTypeKeys []string) error { + if len(workItemTypeKeys) == 0 { + return nil + } + + query := bson.M{"work_item_type_key": bson.M{"$in": workItemTypeKeys}} + _, err := c.DeleteMany(context.TODO(), query) + return err +} diff --git a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_task_v4.go b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_task_v4.go index 31bd4aa67b..8955f67050 100644 --- a/pkg/microservice/aslan/core/common/repository/mongodb/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/common/repository/mongodb/workflow_task_v4.go @@ -43,6 +43,18 @@ type ListWorkflowTaskV4Option struct { Limit int Skip int IsSort bool + + LarkWorkItemTypeKey string + LarkWorkItemID string +} + +type WorkflowNameAndID struct { + WorkflowName string + TaskID int64 +} + +type ListWorkflowTaskV4ByNameAndIDsOption struct { + WorkflowNameAndIDs []WorkflowNameAndID } type WorkflowTaskv4Coll struct { @@ -84,6 +96,16 @@ func (c *WorkflowTaskv4Coll) EnsureIndex(ctx context.Context) error { }, Options: options.Index().SetUnique(false), }, + { + Keys: bson.D{ + bson.E{Key: "workflow_name", Value: 1}, + bson.E{Key: "task_id", Value: 1}, + bson.E{Key: "lark_workitem_type_key", Value: 1}, + bson.E{Key: "lark_workitem_id", Value: 1}, + bson.E{Key: "is_deleted", Value: 1}, + }, + Options: options.Index().SetUnique(false).SetName("lark_workitem_task_index"), + }, } _, err := c.Indexes().CreateMany(ctx, mod) @@ -125,6 +147,13 @@ func (c *WorkflowTaskv4Coll) List(opt *ListWorkflowTaskV4Option) ([]*models.Work if opt.Type != "" { query["type"] = opt.Type } + if opt.LarkWorkItemTypeKey != "" { + query["lark_workitem_type_key"] = opt.LarkWorkItemTypeKey + } + if opt.LarkWorkItemID != "" { + query["lark_workitem_id"] = opt.LarkWorkItemID + } + query["is_archived"] = false query["is_deleted"] = false if opt.CreateTime > 0 { @@ -157,6 +186,38 @@ func (c *WorkflowTaskv4Coll) List(opt *ListWorkflowTaskV4Option) ([]*models.Work return resp, count, nil } +func (c *WorkflowTaskv4Coll) ListByNameAndIDs(opt *ListWorkflowTaskV4ByNameAndIDsOption) ([]*models.WorkflowTask, error) { + resp := make([]*models.WorkflowTask, 0) + query := bson.M{} + + // 构建 $or 条件来查询多个 workflow_name 和 task_id 的组合 + orConditions := make([]bson.M, 0, len(opt.WorkflowNameAndIDs)) + for _, workflowNameAndID := range opt.WorkflowNameAndIDs { + orConditions = append(orConditions, bson.M{ + "workflow_name": workflowNameAndID.WorkflowName, + "task_id": workflowNameAndID.TaskID, + }) + } + + if len(orConditions) > 0 { + query["$or"] = orConditions + } + + query["is_archived"] = false + query["is_deleted"] = false + findOption := options.Find() + + cursor, err := c.Collection.Find(context.TODO(), query, findOption) + if err != nil { + return nil, err + } + err = cursor.All(context.TODO(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + func (c *WorkflowTaskv4Coll) GetLatest(workflowName string) (*models.WorkflowTask, error) { resp := new(models.WorkflowTask) query := bson.M{} diff --git a/pkg/microservice/aslan/core/plugin/handler/lark.go b/pkg/microservice/aslan/core/plugin/handler/lark.go new file mode 100644 index 0000000000..f930e6c79d --- /dev/null +++ b/pkg/microservice/aslan/core/plugin/handler/lark.go @@ -0,0 +1,616 @@ +/* +Copyright 2024 The KodeRover Authors. + +Licensed 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 handler + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/koderover/zadig/v2/pkg/config" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/plugin/service" + internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" + "github.com/koderover/zadig/v2/pkg/tool/cache" + e "github.com/koderover/zadig/v2/pkg/tool/errors" + "github.com/koderover/zadig/v2/pkg/types" +) + +// @summary 飞书插件登录 +// @description 飞书插件登录 +// @tags plugin +// @accept json +// @produce json +// @Param body body service.LarkLoginRequest false "登录请求" +// @success 200 {object} service.LarkLoginResponse +// @router /api/plugin/lark/login [post] +func LarkLogin(c *gin.Context) { + ctx := internalhandler.NewContext(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + err := commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + req := new(service.LarkLoginRequest) + if err := c.BindJSON(req); err != nil { + ctx.RespErr = err + return + } + + workspaceID := c.GetHeader("X-WORKSPACE-ID") + if workspaceID == "" { + ctx.RespErr = fmt.Errorf("missing X-WORKSPACE-ID") + return + } + + ctx.Resp, ctx.RespErr = service.LarkLogin(ctx, workspaceID, req) +} + +// @summary 获取飞书插件授权配置 +// @description 获取飞书插件授权配置 +// @tags plugin +// @accept json +// @produce json +// @Param workspace_id query string true "工作空间ID" +// @success 200 {object} models.LarkPluginAuthConfig +// @router /api/plugin/lark/config/auth [get] +func GetLarkAuthConfig(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + workspaceID := c.Query("workspace_id") + if workspaceID == "" { + ctx.RespErr = fmt.Errorf("workspace_id is required") + return + } + + ctx.Resp, ctx.RespErr = service.GetLarkAuthConfig(ctx, workspaceID) +} + +// @summary 更新飞书插件授权配置 +// @description 更新飞书插件授权配置 +// @tags plugin +// @accept json +// @produce json +// @Param body body models.LarkPluginAuthConfig false "授权配置" +// @success 200 +// @router /api/plugin/lark/config/auth [put] +func UpdateLarkAuthConfig(c *gin.Context) { + ctx := internalhandler.NewContext(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + err := commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + req := new(models.LarkPluginAuthConfig) + if err := c.BindJSON(req); err != nil { + ctx.RespErr = err + return + } + + ctx.RespErr = service.UpdateLarkAuthConfig(ctx, req) +} + +// @summary 获取飞书插件工作流配置 +// @description 获取飞书插件工作流配置 +// @tags plugin +// @accept json +// @produce json +// @Param workspace_id query string true "工作空间ID" +// @success 200 {object} service.GetLarkWorkflowConfigResp +// @router /api/plugin/lark/config/workflow [get] +func GetLarkWorkflowConfig(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + ctx.Resp, ctx.RespErr = service.GetLarkWorkflowConfig(ctx) +} + +// @summary 更新飞书插件工作流配置 +// @description 获取飞书插件工作流配置 +// @tags plugin +// @accept json +// @produce json +// @Param configs body service.UpdateLarkWorkflowConfigRequest false "工作流配置" +// @success 200 +// @router /api/plugin/lark/config/workflow [put] +func UpdateLarkWorkflowConfig(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + req := new(service.UpdateLarkWorkflowConfigRequest) + if err := c.BindJSON(req); err != nil { + ctx.RespErr = err + return + } + + ctx.RespErr = service.UpdateLarkWorkflowConfig(ctx, req) +} + +// @summary 获取飞书插件工作项类型 +// @description 获取飞书插件工作项类型 +// @tags plugin +// @accept json +// @produce json +// @success 200 {object} service.GetLarkWorkitemTypeResponse +// @router /api/plugin/lark/workitem/type [get] +func GetLarkWorkitemType(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + ctx.Resp, ctx.RespErr = service.GetLarkWorkitemType(ctx) +} + +// @summary 获取飞书插件工作项模版 +// @description 获取飞书插件工作项模版 +// @tags plugin +// @accept json +// @produce json +// @Param workitemTypeKey path string true "workitem type key" +// @success 200 {object} service.GetLarkWorkitemTypeTemplateResponse +// @router /api/plugin/lark/workitem/type/:workitemTypeKey/template [get] +func GetLarkWorkitemTypeTemplate(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + workitemTypeKey := c.Param("workitemTypeKey") + if workitemTypeKey == "" { + ctx.RespErr = fmt.Errorf("workitemTypeKey is required") + return + } + + ctx.Resp, ctx.RespErr = service.GetLarkWorkitemTypeTemplate(ctx, workitemTypeKey) +} + +// @summary 获取飞书插件工作项节点 +// @description 获取飞书插件工作项节点 +// @tags plugin +// @accept json +// @produce json +// @Param workitemTypeKey path string true "workitem type key" +// @success 200 {object} service.GetLarkWorkitemTypeNodesResponse +// @router /api/plugin/lark/workitem/type/:workitemTypeKey/template/:templateID/node [get] +func GetLarkWorkitemTypeNodes(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + workitemTypeKey := c.Param("workitemTypeKey") + if workitemTypeKey == "" { + ctx.RespErr = fmt.Errorf("workitemTypeKey is required") + return + } + + templateIDStr := c.Param("templateID") + if templateIDStr == "" { + ctx.RespErr = fmt.Errorf("templateID is required") + return + } + + templateID, err := strconv.ParseInt(templateIDStr, 10, 64) + if err != nil { + ctx.RespErr = fmt.Errorf("templateID is invalid, error: %w", err) + return + } + + ctx.Resp, ctx.RespErr = service.GetLarkWorkitemTypeNodes(ctx, workitemTypeKey, templateID) +} + +// @summary 获取飞书插件工作项的工作流 +// @description 获取飞书插件工作项的工作流 +// @tags plugin +// @accept json +// @produce json +// @Param workitemTypeKey path string true "workitem type key" +// @Param workItemID path string true "workitem id" +// @success 200 {object} service.GetLarkWorkitemWorkflowResponse +// @router /api/plugin/lark/workitem/:workitemTypeKey/:workItemID/workflow [get] +func GetLarkWorkitemWorkflow(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + workflowTypeKey := c.Param("workitemTypeKey") + if workflowTypeKey == "" { + ctx.RespErr = fmt.Errorf("workflowTypeKey is required") + return + } + + workItemID := c.Param("workItemID") + if workItemID == "" { + ctx.RespErr = fmt.Errorf("workItemID is required") + return + } + + collModeWorkflowsWithVerb, err := internalhandler.ListAuthorizedWorkflowWithVerb(ctx.UserID, "") + if err != nil { + ctx.Logger.Errorf("failed to list collaboration mode authorized workflow resource, error: %s", err) + ctx.RespErr = err + return + } + + authProjects := sets.Set[string]{} + authWorkflows := sets.Set[string]{} + + for projectName, project := range ctx.Resources.ProjectAuthInfo { + if project.IsProjectAdmin || project.Workflow.Execute { + authProjects.Insert(projectName) + } + } + + for _, workflowMap := range collModeWorkflowsWithVerb.ProjectWorkflowActionsMap { + for workflowName, workflowAction := range workflowMap { + if workflowAction.Execute { + authWorkflows.Insert(workflowName) + } + } + } + + ctx.Resp, ctx.RespErr = service.GetLarkWorkitemWorkflow(ctx, workflowTypeKey, workItemID, ctx.Resources.IsSystemAdmin, authProjects, authWorkflows) +} + +// @summary 执行飞书插件工作项的工作流 +// @description 执行飞书插件工作项的工作流 +// @tags plugin +// @accept json +// @produce json +// @Param workitemTypeKey path string true "workitem type key" +// @Param workItemID path string true "workitem id" +// @success 200 {object} service.GetLarkWorkitemWorkflowResponse +// @router /api/plugin/lark/workitem/:workitemTypeKey/:workItemID/workflow [post] +func ExecuteLarkWorkitemWorkflow(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + workItemTypeKey := c.Param("workitemTypeKey") + if workItemTypeKey == "" { + ctx.RespErr = fmt.Errorf("workitemTypeKey is required") + return + } + + workItemID := c.Param("workItemID") + if workItemID == "" { + ctx.RespErr = fmt.Errorf("workItemID is required") + return + } + + args := new(models.WorkflowV4) + err = c.BindJSON(args) + if err != nil { + ctx.RespErr = e.ErrInvalidParam.AddErr(fmt.Errorf("get bind json error: %v", err)) + return + } + + if !ctx.Resources.IsSystemAdmin { + if _, ok := ctx.Resources.ProjectAuthInfo[args.Project]; !ok { + ctx.UnAuthorized = true + return + } + + if !ctx.Resources.ProjectAuthInfo[args.Project].IsProjectAdmin && + !ctx.Resources.ProjectAuthInfo[args.Project].Workflow.Execute { + // check if the permission is given by collaboration mode + permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, args.Project, types.ResourceTypeWorkflow, args.Name, types.WorkflowActionRun) + if err != nil || !permitted { + ctx.UnAuthorized = true + return + } + } + } + + ctx.RespErr = service.ExecuteLarkWorkitemWorkflow(ctx, workItemTypeKey, workItemID, args) +} + +// @summary 列出飞书插件工作项的工作流任务 +// @description 列出飞书插件工作项的工作流任务 +// @tags plugin +// @accept json +// @produce json +// @Param workflowName path string true "workflow name" +// @Param workitemTypeKey path string true "workitem type key" +// @Param workItemID path string true "workitem id" +// @Param pageNum query int true "page num" +// @param pageSize query int true "page size" +// @success 200 {object} service.ListLarkWorkitemWorkflowTaskResponse +// @router /api/plugin/lark/workitem/:workitemTypeKey/:workItemID/workflow/:workflowName/task [get] +func ListLarkWorkitemWorkflowTask(c *gin.Context) { + ctx, err := internalhandler.NewContextWithAuthorization(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + if err != nil { + ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err) + ctx.UnAuthorized = true + return + } + + err = commonutil.CheckZadigEnterpriseLicense() + if err != nil { + ctx.RespErr = err + return + } + + err = CheckLarkAuth(c, ctx) + if err != nil { + ctx.RespErr = err + ctx.UnAuthorized = true + return + } + + workflowTypeKey := c.Param("workitemTypeKey") + if workflowTypeKey == "" { + ctx.RespErr = fmt.Errorf("workflowTypeKey is required") + return + } + + workItemID := c.Param("workItemID") + if workItemID == "" { + ctx.RespErr = fmt.Errorf("workItemID is required") + return + } + + workflowName := c.Param("workflowName") + if workflowName == "" { + ctx.RespErr = fmt.Errorf("workflowName is required") + return + } + + pageNumStr := c.Query("pageNum") + if pageNumStr == "" { + ctx.RespErr = fmt.Errorf("pageNum is required") + return + } + pageNum, err := strconv.Atoi(pageNumStr) + if err != nil { + ctx.RespErr = fmt.Errorf("pageNum is invalid, error: %w", err) + return + } + + pageSizeStr := c.Query("pageSize") + if pageSizeStr == "" { + ctx.RespErr = fmt.Errorf("pageSize is required") + return + } + pageSize, err := strconv.Atoi(pageSizeStr) + if err != nil { + ctx.RespErr = fmt.Errorf("pageSize is invalid, error: %w", err) + return + } + + ctx.Resp, ctx.RespErr = service.ListLarkWorkitemWorkflowTask(ctx, workflowTypeKey, workItemID, workflowName, pageNum, pageSize) +} + +func CheckLarkAuth(c *gin.Context, ctx *internalhandler.Context) (err error) { + defer func() { + if err != nil { + ctx.Logger.Errorf("CheckLarkAuth failed: %s", err) + } + }() + + token := c.GetHeader("X-PLUGIN-TOKEN") + if token == "" { + err = fmt.Errorf("Unauthorized, missing X-PLUGIN-TOKEN") + return + } + + userKey := c.GetHeader("X-USER-KEY") + if userKey == "" { + err = fmt.Errorf("Unauthorized, missing X-USER-KEY") + return + } + + workspaceID := c.GetHeader("X-WORKSPACE-ID") + if workspaceID == "" { + err = fmt.Errorf("Unauthorized, missing X-USER-KEY") + return + } + + userTokenStr, err := cache.NewRedisCache(config.RedisCommonCacheTokenDB()).GetString(fmt.Sprintf("lark-plugin-user-token-%s-%s", workspaceID, userKey)) + if err != nil { + if errors.Is(err, redis.Nil) { + err = fmt.Errorf("Unauthorized, user token not found") + return + } + err = fmt.Errorf("Unauthorized, failed to get user token from cache: %w", err) + return + } + + if userTokenStr == "" { + err = fmt.Errorf("Unauthorized, user token not found") + return + } + + userToken := &service.LarkLoginResponse{} + err = json.Unmarshal([]byte(userTokenStr), userToken) + if err != nil { + err = fmt.Errorf("Unauthorized, missing X-PLUGIN-TOKEN") + return + } + + if userToken.UserAccessToken != token { + err = fmt.Errorf("Unauthorized, token mismatch") + return + } + + if userToken.UserKey != userKey { + err = fmt.Errorf("Unauthorized, user key mismatch") + return + } + + ctx.LarkPlugin = &internalhandler.LarkPluginContext{ + PluginAccessToken: userToken.PluginAccessToken, + UserKey: userKey, + ProjectKey: workspaceID, + LarkType: userToken.LarkType, + } + + return nil +} diff --git a/pkg/microservice/aslan/core/plugin/handler/router.go b/pkg/microservice/aslan/core/plugin/handler/router.go new file mode 100644 index 0000000000..ec20e42d80 --- /dev/null +++ b/pkg/microservice/aslan/core/plugin/handler/router.go @@ -0,0 +1,38 @@ +/* +Copyright 2024 The KodeRover Authors. + +Licensed 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 handler + +import "github.com/gin-gonic/gin" + +type Router struct{} + +func (*Router) Inject(router *gin.RouterGroup) { + lark := router.Group("lark") + { + lark.POST("login", LarkLogin) + lark.GET("config/auth", GetLarkAuthConfig) + lark.PUT("config/auth", UpdateLarkAuthConfig) + lark.GET("config/workflow", GetLarkWorkflowConfig) + lark.PUT("config/workflow", UpdateLarkWorkflowConfig) + lark.GET("workitem/type", GetLarkWorkitemType) + lark.GET("workitem/type/:workitemTypeKey/template", GetLarkWorkitemTypeTemplate) + lark.GET("workitem/type/:workitemTypeKey/template/:templateID/node", GetLarkWorkitemTypeNodes) + lark.GET("workitem/:workitemTypeKey/:workItemID/workflow", GetLarkWorkitemWorkflow) + lark.POST("workitem/:workitemTypeKey/:workItemID/workflow", ExecuteLarkWorkitemWorkflow) + lark.GET("workitem/:workitemTypeKey/:workItemID/workflow/:workflowName/task", ListLarkWorkitemWorkflowTask) + } +} diff --git a/pkg/microservice/aslan/core/plugin/service/lark.go b/pkg/microservice/aslan/core/plugin/service/lark.go new file mode 100644 index 0000000000..c1e1c18639 --- /dev/null +++ b/pkg/microservice/aslan/core/plugin/service/lark.go @@ -0,0 +1,631 @@ +/* +Copyright 2024 The KodeRover Authors. + +Licensed 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 service + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "time" + + sdkcore "github.com/larksuite/project-oapi-sdk-golang/core" + "github.com/larksuite/project-oapi-sdk-golang/v2/service/workitem" + "go.mongodb.org/mongo-driver/mongo" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/koderover/zadig/v2/pkg/config" + aslanconfig "github.com/koderover/zadig/v2/pkg/microservice/aslan/config" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + commonmodels "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models" + "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb" + commonutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" + workflowservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/workflow/service/workflow" + internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" + "github.com/koderover/zadig/v2/pkg/tool/cache" + "github.com/koderover/zadig/v2/pkg/tool/larkplugin" + "github.com/koderover/zadig/v2/pkg/tool/log" + "github.com/koderover/zadig/v2/pkg/types" +) + +type LarkLoginRequest struct { + LarkType string `json:"lark_type"` + Code string `json:"code"` +} + +type LarkLoginResponse struct { + UserAccessTokenExpireTime int `json:"user_access_token_expire_time"` + UserAccessToken string `json:"user_access_token"` + RefreshToken string `json:"refresh_token"` + RefreshTokenExpireTime int `json:"refresh_token_expire_time"` + UserKey string `json:"user_key"` + TenantKey string `json:"saas_tenant_key"` + LarkType string `json:"lark_type"` + PluginAccessToken string `json:"plugin_access_token"` + PluginAccessTokenExpireTime int `json:"plugin_access_token_expire_time"` +} + +func LarkLogin(ctx *internalhandler.Context, workspaceID string, req *LarkLoginRequest) (*LarkLoginResponse, error) { + lark := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), req.LarkType) + resp, err := lark.Client.Plugin.GetPluginToken(ctx, config.LarkPluginAccessTokenType()) + if err != nil { + return nil, fmt.Errorf("failed to get lark plugin token: %w", err) + } + + if resp.Error != nil && resp.Error.Code != 0 { + return nil, fmt.Errorf("failed to get lark plugin token, error code: %d, message: %s", resp.Error.Code, resp.Error.Msg) + } + + resp2, err := lark.Client.Plugin.GetUserPluginToken(ctx, req.Code, sdkcore.WithAccessToken(resp.Data.Token)) + if err != nil { + return nil, fmt.Errorf("failed to get lark user plugin token: %w", err) + } + + if resp2.Error != nil && resp2.Error.Code != 0 { + return nil, fmt.Errorf("failed to get lark user plugin token, error code: %d, message: %s", resp2.Error.Code, resp2.Error.Msg) + } + + resp3 := &LarkLoginResponse{ + UserAccessTokenExpireTime: resp2.Data.ExpireTime, + UserAccessToken: resp2.Data.Token, + RefreshToken: resp2.Data.RefreshToken, + RefreshTokenExpireTime: resp2.Data.RefreshTokenExpireTime, + UserKey: resp2.Data.UserKey, + TenantKey: resp2.Data.TenantKey, + PluginAccessToken: resp.Data.Token, + PluginAccessTokenExpireTime: resp.Data.ExpireTime, + LarkType: req.LarkType, + } + + tokenData, err := json.Marshal(resp3) + if err != nil { + return nil, fmt.Errorf("failed to marshal lark user plugin token: %w", err) + } + + err = cache.NewRedisCache(config.RedisCommonCacheTokenDB()).Write(fmt.Sprintf("lark-plugin-user-token-%s-%s", workspaceID, resp2.Data.UserKey), string(tokenData), time.Duration(resp3.PluginAccessTokenExpireTime-5)*time.Second) + if err != nil { + return nil, fmt.Errorf("failed to write lark user plugin token to cache: %w", err) + } + + return resp3, nil +} + +func GetLarkAuthConfig(ctx *internalhandler.Context, workspaceID string) (*commonmodels.LarkPluginAuthConfig, error) { + config, err := mongodb.NewLarkPluginAuthConfigColl().Get(workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to get lark plugin auth config: %w", err) + } + return config, nil +} + +func UpdateLarkAuthConfig(ctx *internalhandler.Context, config *commonmodels.LarkPluginAuthConfig) error { + err := mongodb.NewLarkPluginAuthConfigColl().Update(config) + if err != nil { + return fmt.Errorf("failed to update lark plugin auth config: %w", err) + } + return nil +} + +type GetLarkWorkflowConfigResp struct { + Configs []*commonmodels.LarkPluginWorkflowConfig `json:"configs"` +} + +func GetLarkWorkflowConfig(ctx *internalhandler.Context) (*GetLarkWorkflowConfigResp, error) { + workspaceID := ctx.LarkPlugin.ProjectKey + configs, err := mongodb.NewLarkPluginWorkflowConfigColl().Get(workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to get lark plugin workflow config: %w", err) + } + + resp := &GetLarkWorkflowConfigResp{ + Configs: configs, + } + + return resp, nil +} + +type UpdateLarkWorkflowConfigRequest struct { + WorkspaceID string `json:"workspace_id"` + Configs []*commonmodels.LarkPluginWorkflowConfig `json:"configs"` +} + +func UpdateLarkWorkflowConfig(ctx *internalhandler.Context, req *UpdateLarkWorkflowConfigRequest) error { + updateWorkitemTypeKeySet := sets.Set[string]{} + for _, config := range req.Configs { + config.WorkspaceID = req.WorkspaceID + updateWorkitemTypeKeySet.Insert(config.WorkItemTypeKey) + } + + origConfigs, err := mongodb.NewLarkPluginWorkflowConfigColl().Get(req.WorkspaceID) + if err != nil { + return fmt.Errorf("failed to get lark plugin workflow config: %w", err) + } + + // delete the configs that are not in the request + deleteWorkitemTypeKeys := make([]string, 0) + for _, config := range origConfigs { + if !updateWorkitemTypeKeySet.Has(config.WorkItemTypeKey) { + deleteWorkitemTypeKeys = append(deleteWorkitemTypeKeys, config.WorkItemTypeKey) + } + } + + err = mongodb.NewLarkPluginWorkflowConfigColl().Delete(deleteWorkitemTypeKeys) + if err != nil { + return fmt.Errorf("failed to delete lark plugin workflow config: %w", err) + } + + err = mongodb.NewLarkPluginWorkflowConfigColl().Update(req.Configs) + if err != nil { + return fmt.Errorf("failed to update lark plugin workflow config: %w", err) + } + + return nil +} + +type GetLarkWorkitemTypeResponse struct { + WorkItemTypes []workitem.WorkItemKeyType `json:"work_item_types"` +} + +func GetLarkWorkitemType(ctx *internalhandler.Context) (*GetLarkWorkitemTypeResponse, error) { + projectKey := ctx.LarkPlugin.ProjectKey + client := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), ctx.LarkPlugin.LarkType) + larkResp, err := client.ClientV2.WorkItem.QueryAWorkItemTypes(ctx, workitem.NewQueryAWorkItemTypesReqBuilder(). + ProjectKey(projectKey). + Build(), + sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken), + sdkcore.WithUserKey(ctx.LarkPlugin.UserKey), + ) + if err != nil { + return nil, fmt.Errorf("failed to get lark workitem type: %w", err) + } + + if larkResp.Code() != 0 { + return nil, fmt.Errorf("failed to get lark workitem type, code: %d, message: %s", larkResp.Code(), larkResp.ErrMsg) + } + + resp := &GetLarkWorkitemTypeResponse{ + WorkItemTypes: larkResp.Data, + } + + return resp, nil +} + +type GetLarkWorkitemTypeTemplateResponse struct { + Templates []*LarkWorkitemTypeTemplate `json:"templates"` +} + +type LarkWorkitemTypeTemplate struct { + TemplateID string `json:"template_id"` + TemplateName string `json:"template_name"` + IsDisabled int32 `json:"is_disabled"` + Version int64 `json:"version"` + UniqueKey string `json:"unique_key"` + TemplateKey string `json:"template_key"` +} + +func GetLarkWorkitemTypeTemplate(ctx *internalhandler.Context, workitemTypeKey string) (*GetLarkWorkitemTypeTemplateResponse, error) { + projectKey := ctx.LarkPlugin.ProjectKey + client := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), ctx.LarkPlugin.LarkType) + larkResp, err := client.ClientV2.WorkItem.ListTemplateConf(ctx, workitem.NewListTemplateConfReqBuilder(). + ProjectKey(projectKey). + WorkItemTypeKey(workitemTypeKey). + Build(), + sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken), + sdkcore.WithUserKey(ctx.LarkPlugin.UserKey), + ) + if err != nil { + return nil, fmt.Errorf("failed to get lark workitem type template: %w", err) + } + + if larkResp.Code() != 0 { + return nil, fmt.Errorf("failed to get lark workitem type template, code: %d, message: %s", larkResp.Code(), larkResp.ErrMsg) + } + + templates := make([]*LarkWorkitemTypeTemplate, 0) + for _, template := range larkResp.Data { + templates = append(templates, &LarkWorkitemTypeTemplate{ + TemplateID: *template.TemplateID, + TemplateName: *template.TemplateName, + IsDisabled: *template.IsDisabled, + Version: *template.Version, + UniqueKey: *template.UniqueKey, + TemplateKey: *template.TemplateKey, + }) + } + + resp := &GetLarkWorkitemTypeTemplateResponse{ + Templates: templates, + } + + return resp, nil +} + +type GetLarkWorkitemTypeNodesResponse struct { + Nodes []*LarkWorkitemTypeNode `json:"nodes"` +} + +type LarkWorkitemTypeNode struct { + // 节点 ID + StateKey string `json:"state_key"` + Name string `json:"name"` +} + +func GetLarkWorkitemTypeNodes(ctx *internalhandler.Context, workitemTypeKey string, templateID int64) (*GetLarkWorkitemTypeNodesResponse, error) { + projectKey := ctx.LarkPlugin.ProjectKey + client := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), ctx.LarkPlugin.LarkType) + larkResp, err := client.ClientV2.WorkItem.QueryTemplateDetail(ctx, (*workitem.QueryTemplateDetailReq)(workitem.NewQueryTemplateDetailReqBuilder(). + TemplateID(templateID). + ProjectKey(projectKey). + Build()), + sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken), + sdkcore.WithUserKey(ctx.LarkPlugin.UserKey), + ) + if err != nil { + return nil, fmt.Errorf("failed to query template detail: %w", err) + } + + if larkResp.Code() != 0 { + return nil, fmt.Errorf("failed to query template detail, code: %d, message: %s", larkResp.Code(), larkResp.ErrMsg) + } + + nodeList := make([]*LarkWorkitemTypeNode, 0) + for _, workflowConf := range larkResp.Data.WorkflowConfs { + node := &LarkWorkitemTypeNode{ + StateKey: *workflowConf.StateKey, + Name: *workflowConf.Name, + } + nodeList = append(nodeList, node) + } + + resp := &GetLarkWorkitemTypeNodesResponse{ + Nodes: nodeList, + } + + return resp, nil +} + +type GetLarkWorkitemWorkflowResponse struct { + Nodes []*NodeWorkflows `json:"nodes"` +} + +type Node struct { + ID string `json:"id"` + Name string `json:"name"` + IsCurrent bool `json:"is_current"` +} + +type NodeWorkflowWithAction struct { + Workflow *models.WorkflowV4 `json:"workflow"` + CanExecute bool `json:"can_execute"` +} + +type NodeWorkflows struct { + Node *Node `json:"node"` + Workflows []*NodeWorkflowWithAction `json:"workflows"` +} + +func GetLarkWorkitemWorkflow(ctx *internalhandler.Context, workItemType, workItemID string, isAdmin bool, authProjects, authWorkflows sets.Set[string]) (*GetLarkWorkitemWorkflowResponse, error) { + workItemIDInt, err := strconv.ParseInt(workItemID, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse workitem id: %w", err) + } + + client := larkplugin.NewClient(config.LarkPluginID(), config.LarkPluginSecret(), ctx.LarkPlugin.LarkType) + larkResp, err := client.ClientV2.WorkItem.GetWorkItemsByIds(ctx, workitem.NewGetWorkItemsByIdsReqBuilder(). + ProjectKey(ctx.LarkPlugin.ProjectKey). + WorkItemTypeKey(workItemType). + WorkItemIDs([]int64{workItemIDInt}). + Build(), + sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken), + sdkcore.WithUserKey(ctx.LarkPlugin.UserKey), + ) + if err != nil { + return nil, fmt.Errorf("failed to get lark workitem type node: %w", err) + } + + if larkResp.Code() != 0 { + return nil, fmt.Errorf("failed to get lark workitem type node, code: %d, message: %s", larkResp.Code(), larkResp.ErrMsg) + } + + if len(larkResp.Data) == 0 { + return nil, fmt.Errorf("workitem could not be found") + } + + workItem := larkResp.Data[0] + + larkResp2, err := client.ClientV2.WorkItem.QueryTemplateDetail(ctx, workitem.NewQueryTemplateDetailReqBuilder(). + TemplateID(*workItem.TemplateID). + ProjectKey(ctx.LarkPlugin.ProjectKey). + Build(), + sdkcore.WithAccessToken(ctx.LarkPlugin.PluginAccessToken), + sdkcore.WithUserKey(ctx.LarkPlugin.UserKey), + ) + if err != nil { + return nil, fmt.Errorf("failed to query template detail: %w", err) + } + if larkResp2.Code() != 0 { + return nil, fmt.Errorf("failed to query template detail, code: %d, error: %s", larkResp2.Code(), larkResp2.ErrMsg) + } + + templateWorkflowConfig := larkResp2.Data.WorkflowConfs + + workflowConfig, err := mongodb.NewLarkPluginWorkflowConfigColl().GetWorkItemTypeConfig(ctx.LarkPlugin.ProjectKey, workItemType) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return &GetLarkWorkitemWorkflowResponse{ + Nodes: make([]*NodeWorkflows, 0), + }, nil + } + return nil, fmt.Errorf("failed to get lark plugin workflow config: %w", err) + } + + // 节点ID和节点工作流的map + NodeIDWorkflowMap := make(map[string]*NodeWorkflows) + // 工作流名称和节点ID的map + NodeIDWorkflowNamesMap := make(map[string][]string) + // 用于搜索数据库中节点配置的工作流 + nodeConfigWorkflows := make([]mongodb.WorkflowV4, 0) + for _, node := range templateWorkflowConfig { + log.Debugf("template workflow node: %s", *node.StateKey) + for _, workflowConfigNode := range workflowConfig.Nodes { + log.Debugf("db workflow config node: %s", workflowConfigNode.NodeID) + if workflowConfigNode.TemplateID != *workItem.TemplateID { + continue + } + + // 仅列出配置了工作流的节点 + if *node.StateKey == workflowConfigNode.NodeID { + isCurrent := false + for _, currentNode := range workItem.CurrentNodes { + if *node.StateKey == *currentNode.ID { + isCurrent = true + } + } + + NodeIDWorkflowMap[workflowConfigNode.NodeID] = &NodeWorkflows{ + Node: &Node{ + ID: workflowConfigNode.NodeID, + Name: workflowConfigNode.NodeName, + IsCurrent: isCurrent, + }, + Workflows: make([]*NodeWorkflowWithAction, 0), + } + + nodeConfigWorkflows = append(nodeConfigWorkflows, mongodb.WorkflowV4{ + Name: workflowConfigNode.WorkflowName, + ProjectName: workflowConfigNode.ProjectKey, + }) + + NodeIDWorkflowNamesMap[workflowConfigNode.NodeID] = append(NodeIDWorkflowNamesMap[workflowConfigNode.NodeID], workflowConfigNode.WorkflowName) + } + } + } + + log.Debugf("nodeConfigWorkflows: %+v", nodeConfigWorkflows) + + workflows, err := mongodb.NewWorkflowV4Coll().ListByWorkflows(mongodb.ListWorkflowV4Opt{ + Workflows: nodeConfigWorkflows, + }) + if err != nil { + return nil, fmt.Errorf("failed to list workflows: %w", err) + } + + workflowMap := make(map[string]*models.WorkflowV4) + for _, workflow := range workflows { + workflowMap[workflow.Name] = workflow + } + + log.Debugf("NodeIDWorkflowNamesMap: %+v", NodeIDWorkflowNamesMap) + + for nodeID, node := range NodeIDWorkflowMap { + workflowNames := NodeIDWorkflowNamesMap[nodeID] + for _, workflowName := range workflowNames { + workflow, ok := workflowMap[workflowName] + if !ok { + continue + } + + workflowWithAction := &NodeWorkflowWithAction{ + Workflow: workflow, + } + + if isAdmin { + workflowWithAction.CanExecute = true + } else if authProjects.Has(workflow.Project) { + workflowWithAction.CanExecute = true + } else if authWorkflows.Has(workflow.Name) { + workflowWithAction.CanExecute = true + } + + node.Workflows = append(node.Workflows, workflowWithAction) + } + } + + // 按照templateWorkflowConfig的顺序对NodeIDWorkflowMap进行排序 + resp := make([]*NodeWorkflows, 0) + for _, node := range templateWorkflowConfig { + if nodeWorkflows, exists := NodeIDWorkflowMap[*node.StateKey]; exists { + resp = append(resp, nodeWorkflows) + } + } + + return &GetLarkWorkitemWorkflowResponse{ + Nodes: resp, + }, nil +} + +type LarkWorkitemWorkflowTask struct { + TaskID int64 `json:"task_id"` + TaskCreator string `json:"task_creator"` + ProjectName string `json:"project_name"` + WorkflowName string `json:"workflow_name"` + WorkflowDisplayName string `json:"workflow_display_name"` + Remark string `json:"remark"` + Status aslanconfig.Status `json:"status"` + Reverted bool `json:"reverted"` + CreateTime int64 `json:"create_time,omitempty"` + StartTime int64 `json:"start_time,omitempty"` + EndTime int64 `json:"end_time,omitempty"` + Hash string `json:"hash"` + Repos []*types.Repository `json:"repos"` + ServiceModules []*commonmodels.ServiceWithModule `json:"service_modules"` + DeployEnvs []*commonmodels.WorkflowEnv `json:"deploy_envs"` +} + +type ListLarkWorkitemWorkflowTaskResponse struct { + Tasks []*LarkWorkitemWorkflowTask `json:"tasks"` + Count int64 `json:"count"` +} + +func ListLarkWorkitemWorkflowTask(ctx *internalhandler.Context, workItemTypeKey, workItemID, workflowName string, pageNum, pageSize int) (*ListLarkWorkitemWorkflowTaskResponse, error) { + tasks, count, err := mongodb.NewworkflowTaskv4Coll().List(&mongodb.ListWorkflowTaskV4Option{ + WorkflowName: workflowName, + LarkWorkItemTypeKey: workItemTypeKey, + LarkWorkItemID: workItemID, + Skip: (pageNum - 1) * pageSize, + Limit: pageSize, + }) + if err != nil { + return nil, fmt.Errorf("failed to list workflow task: %w", err) + } + + envMap := make(map[string]*commonmodels.Product) + resp := make([]*LarkWorkitemWorkflowTask, 0) + for _, task := range tasks { + respTask := &LarkWorkitemWorkflowTask{ + TaskID: task.TaskID, + TaskCreator: task.TaskCreator, + ProjectName: task.ProjectName, + WorkflowName: task.WorkflowName, + WorkflowDisplayName: task.WorkflowDisplayName, + Remark: task.Remark, + Status: task.Status, + Reverted: task.Reverted, + CreateTime: task.CreateTime, + StartTime: task.StartTime, + EndTime: task.EndTime, + Hash: task.Hash, + } + + repoSet := sets.New[string]() + serviceModuleSet := sets.New[string]() + deployEnvSet := sets.New[string]() + + repos := make([]*types.Repository, 0) + serviceModules := make([]*commonmodels.ServiceWithModule, 0) + deployEnvs := make([]*commonmodels.WorkflowEnv, 0) + + updateRepos := func(repo *types.Repository) { + if repoSet.Has(repo.GetKey()) { + return + } + repoSet.Insert(repo.GetKey()) + repos = append(repos, repo) + } + + updateServiceModules := func(serviceModule *commonmodels.ServiceWithModule) { + if serviceModuleSet.Has(serviceModule.GetKey()) { + return + } + serviceModuleSet.Insert(serviceModule.GetKey()) + serviceModules = append(serviceModules, serviceModule) + } + + updateDeployEnvs := func(deployEnv *commonmodels.WorkflowEnv) { + if deployEnvSet.Has(deployEnv.EnvName) { + return + } + deployEnvSet.Insert(deployEnv.EnvName) + deployEnvs = append(deployEnvs, deployEnv) + } + + for _, stage := range task.WorkflowArgs.Stages { + for _, job := range stage.Jobs { + if job.Skipped { + continue + } + + switch job.JobType { + case aslanconfig.JobZadigBuild: + build := new(commonmodels.ZadigBuildJobSpec) + if err := commonmodels.IToi(job.Spec, build); err != nil { + return nil, fmt.Errorf("failed to convert job spec to build job spec: %w", err) + } + + for _, serviceAndBuild := range build.ServiceAndBuilds { + sm := &commonmodels.ServiceWithModule{ + ServiceName: serviceAndBuild.ServiceName, + ServiceModule: serviceAndBuild.ServiceModule, + } + updateServiceModules(sm) + + for _, repo := range serviceAndBuild.Repos { + updateRepos(repo) + } + } + case aslanconfig.JobZadigDeploy: + deploy := new(commonmodels.ZadigDeployJobSpec) + if err := commonmodels.IToi(job.Spec, deploy); err != nil { + return nil, fmt.Errorf("failed to convert job spec to deploy job spec: %w", err) + } + + for _, svc := range deploy.Services { + for _, module := range svc.Modules { + sm := &commonmodels.ServiceWithModule{ + ServiceName: svc.ServiceName, + ServiceModule: module.ServiceModule, + } + updateServiceModules(sm) + } + } + env := &commonmodels.WorkflowEnv{ + EnvName: deploy.Env, + Production: deploy.Production, + EnvAlias: commonutil.GetEnvAlias(commonutil.GetEnvInfoNoErr(task.ProjectName, deploy.Env, envMap)), + } + updateDeployEnvs(env) + } + } + } + + respTask.Repos = repos + respTask.ServiceModules = serviceModules + respTask.DeployEnvs = deployEnvs + + resp = append(resp, respTask) + } + + return &ListLarkWorkitemWorkflowTaskResponse{ + Tasks: resp, + Count: count, + }, nil +} + +func ExecuteLarkWorkitemWorkflow(ctx *internalhandler.Context, workItemTypeKey, workItemID string, args *models.WorkflowV4) error { + _, err := workflowservice.CreateWorkflowTaskV4(&workflowservice.CreateWorkflowTaskV4Args{ + Name: ctx.UserName, + Account: ctx.Account, + UserID: ctx.UserID, + LarkWorkItemTypeKey: workItemTypeKey, + LarkWorkItemID: workItemID, + }, args, ctx.Logger) + if err != nil { + return fmt.Errorf("failed to create workflow task: %w", err) + } + + return nil +} diff --git a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go index ab266c4d02..1fb53ffe78 100644 --- a/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go +++ b/pkg/microservice/aslan/core/workflow/service/workflow/workflow_task_v4.go @@ -443,12 +443,14 @@ func CheckWorkflowV4ApprovalInitiator(workflowName, uid string, log *zap.Sugared } type CreateWorkflowTaskV4Args struct { - Name string - Account string - UserID string - Type config.CustomWorkflowTaskType - ApprovalTicketID string - SkipWorkflowUpdate bool + Name string + Account string + UserID string + Type config.CustomWorkflowTaskType + ApprovalTicketID string + SkipWorkflowUpdate bool + LarkWorkItemTypeKey string + LarkWorkItemID string } func CreateWorkflowTaskV4ByBuildInTrigger(triggerName string, args *commonmodels.WorkflowV4, log *zap.SugaredLogger) (*CreateTaskV4Resp, error) { @@ -591,6 +593,8 @@ func CreateWorkflowTaskV4(args *CreateWorkflowTaskV4Args, workflow *commonmodels workflowTask.ShareStorages = workflow.ShareStorages workflowTask.IsDebug = workflow.Debug workflowTask.Remark = workflow.Remark + workflowTask.LarkWorkItemID = args.LarkWorkItemID + workflowTask.LarkWorkItemTypeKey = args.LarkWorkItemTypeKey workflowCtrl := workflowController.CreateWorkflowController(workflow) if (args.Type == config.WorkflowTaskTypeWorkflow || args.Type == "") && !args.SkipWorkflowUpdate { diff --git a/pkg/microservice/aslan/server/rest/router.go b/pkg/microservice/aslan/server/rest/router.go index 310112be55..5e0c10afd3 100644 --- a/pkg/microservice/aslan/server/rest/router.go +++ b/pkg/microservice/aslan/server/rest/router.go @@ -36,6 +36,7 @@ import ( loghandler "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/log/handler" multiclusterhandler "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/multicluster/handler" clusterservice "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/multicluster/service" + pluginhandler "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/plugin/handler" projecthandler "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/project/handler" releaseplanhandler "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/release_plan/handler" servicehandler "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/service/handler" @@ -148,6 +149,7 @@ func (s *engine) injectRouterGroup(router *gin.RouterGroup) { "/api/cache": cachehandler.NewRouter(), "/api/vm": new(vmhandler.Router), "/api/ticket": new(tickethandler.Router), + "/api/plugin": new(pluginhandler.Router), } { r.Inject(router.Group(name)) } diff --git a/pkg/setting/consts.go b/pkg/setting/consts.go index 904b876973..51c0289d6d 100644 --- a/pkg/setting/consts.go +++ b/pkg/setting/consts.go @@ -48,11 +48,14 @@ const ( ENVNamespace = "BE_POD_NAMESPACE" // Aslan - ENVLogLevel = "LOG_LEVEL" - ENVExecutorLogLevel = "EXECUTOR_LOG_LEVEL" - ENVServiceStartTimeout = "SERVICE_START_TIMEOUT" - ENVDefaultEnvRecycleDay = "DEFAULT_ENV_RECYCLE_DAY" - ENVDefaultIngressClass = "DEFAULT_INGRESS_CLASS" + ENVLogLevel = "LOG_LEVEL" + ENVExecutorLogLevel = "EXECUTOR_LOG_LEVEL" + ENVServiceStartTimeout = "SERVICE_START_TIMEOUT" + ENVDefaultEnvRecycleDay = "DEFAULT_ENV_RECYCLE_DAY" + ENVDefaultIngressClass = "DEFAULT_INGRESS_CLASS" + ENVLarkPluginID = "LARK_PLUGIN_ID" + ENVLarkPluginSecret = "LARK_PLUGIN_SECRET" + ENVLarkPluginAccessTokenType = "LARK_PLUGIN_ACCESS_TOKEN_TYPE" ENVBuildBaseImage = "BUILD_BASE_IMAGE" diff --git a/pkg/shared/handler/base.go b/pkg/shared/handler/base.go index 34dd9c028d..e6aeed91fe 100644 --- a/pkg/shared/handler/base.go +++ b/pkg/shared/handler/base.go @@ -55,6 +55,14 @@ type Context struct { IdentityType string RequestID string Resources *user.AuthorizedResources + LarkPlugin *LarkPluginContext +} + +type LarkPluginContext struct { + PluginAccessToken string + UserKey string + ProjectKey string + LarkType string } type jwtClaims struct { @@ -255,6 +263,8 @@ func JSONResponse(c *gin.Context, ctx *Context) { if ctx.UnAuthorized { if ctx.RespErr != nil { c.Set(setting.ResponseError, ctx.RespErr) + c.AbortWithError(http.StatusForbidden, ctx.RespErr) + return } c.AbortWithStatus(http.StatusForbidden) return @@ -401,7 +411,20 @@ func responseHelper(response interface{}) interface{} { updatedField := reflect.ValueOf(responseHelper(valField.Interface())) if valField.Kind() == reflect.Ptr { if updatedField.IsValid() { - newValField.Set(updatedField) + // 如果目标字段是指针类型,需要确保updatedField也是指针类型 + if updatedField.Kind() == reflect.Ptr { + newValField.Set(updatedField) + } else { + // 如果updatedField不是指针,需要创建指向它的指针 + if updatedField.CanAddr() { + newValField.Set(updatedField.Addr()) + } else { + // 如果无法取地址,创建一个新的指针 + ptr := reflect.New(updatedField.Type()) + ptr.Elem().Set(updatedField) + newValField.Set(ptr) + } + } } } else { if updatedField.IsValid() { diff --git a/pkg/tool/larkplugin/client.go b/pkg/tool/larkplugin/client.go new file mode 100644 index 0000000000..2e5cb85d5e --- /dev/null +++ b/pkg/tool/larkplugin/client.go @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The KodeRover Authors. + * + * Licensed 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 larkplugin + +import ( + "net/http" + "time" + + "github.com/koderover/zadig/v2/pkg/config" + sdk "github.com/larksuite/project-oapi-sdk-golang" + sdkcore "github.com/larksuite/project-oapi-sdk-golang/core" +) + +type Client struct { + *sdk.Client + *sdk.ClientV2 +} + +func NewClient(pluginID, pluginSecret, larkType string) *Client { + return &Client{ + Client: sdk.NewClient(pluginID, pluginSecret, + sdk.WithReqTimeout(3*time.Second), + sdk.WithEnableTokenCache(true), + sdk.WithHttpClient(http.DefaultClient), + sdk.WithOpenBaseUrl(GetLarkPluginBaseUrl(larkType)), + sdk.WithAccessTokenType(sdkcore.AccessTokenType(config.LarkPluginAccessTokenType())), + ), + ClientV2: sdk.NewClientV2(pluginID, pluginSecret, + sdk.WithReqTimeout(3*time.Second), + sdk.WithEnableTokenCache(true), + sdk.WithHttpClient(http.DefaultClient), + sdk.WithOpenBaseUrl(GetLarkPluginBaseUrl(larkType)), + sdk.WithAccessTokenType(sdkcore.AccessTokenType(config.LarkPluginAccessTokenType())), + ), + } +} diff --git a/pkg/tool/larkplugin/token.go b/pkg/tool/larkplugin/token.go new file mode 100644 index 0000000000..cfba6c378d --- /dev/null +++ b/pkg/tool/larkplugin/token.go @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The KodeRover Authors. + * + * Licensed 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 larkplugin diff --git a/pkg/tool/larkplugin/tool.go b/pkg/tool/larkplugin/tool.go new file mode 100644 index 0000000000..e88dfc54b3 --- /dev/null +++ b/pkg/tool/larkplugin/tool.go @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The KodeRover Authors. + * + * Licensed 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 larkplugin + +import ( + "github.com/koderover/zadig/v2/pkg/setting" +) + +func GetLarkPluginBaseUrl(larkType string) string { + if larkType == setting.IMLark { + return "https://project.feishu.cn" + } else if larkType == setting.IMLarkIntl { + return "" + } + + return "" +} diff --git a/pkg/tool/larkplugin/types.go b/pkg/tool/larkplugin/types.go new file mode 100644 index 0000000000..471b9e7ae3 --- /dev/null +++ b/pkg/tool/larkplugin/types.go @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The KodeRover Authors. + * + * Licensed 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 larkplugin