Merge branch 'release-v0.21' into lw-cp0223
diff --git a/backend/plugins/zentao/models/story.go b/backend/plugins/zentao/models/story.go
index 661a626..42956b7 100644
--- a/backend/plugins/zentao/models/story.go
+++ b/backend/plugins/zentao/models/story.go
@@ -22,60 +22,60 @@
)
type ZentaoStoryRes struct {
- ID int64 `json:"id"`
- Vision string `json:"vision"`
- Parent int64 `json:"parent"`
- Product int64 `json:"product"`
- Branch int `json:"branch"`
- Module int `json:"module"`
- Plan string `json:"plan"`
- Source string `json:"source"`
- SourceNote string `json:"sourceNote"`
- FromBug int `json:"fromBug"`
- Feedback int `json:"feedback"`
- Title string `json:"title"`
- Keywords string `json:"keywords"`
- Type string `json:"type"`
- Category string `json:"category"`
- Pri int `json:"pri"`
- Estimate float64 `json:"estimate"`
- Status string `json:"status"`
- SubStatus string `json:"subStatus"`
- Color string `json:"color"`
- Stage string `json:"stage"`
- Mailto []interface{} `json:"mailto"`
- Lib int `json:"lib"`
- FromStory int64 `json:"fromStory"`
- FromVersion int `json:"fromVersion"`
- OpenedBy *ApiAccount `json:"openedBy"`
- OpenedDate *common.Iso8601Time `json:"openedDate"`
- AssignedTo *ApiAccount `json:"assignedTo"`
- AssignedDate *common.Iso8601Time `json:"assignedDate"`
- ApprovedDate *common.Iso8601Time `json:"approvedDate"`
- LastEditedBy *ApiAccount `json:"lastEditedBy"`
- LastEditedDate *common.Iso8601Time `json:"lastEditedDate"`
- ChangedBy string `json:"changedBy"`
- ChangedDate *common.Iso8601Time `json:"changedDate"`
- ReviewedBy *ApiAccount `json:"reviewedBy"`
- ReviewedDate *common.Iso8601Time `json:"reviewedDate"`
- ClosedBy *ApiAccount `json:"closedBy"`
- ClosedDate *common.Iso8601Time `json:"closedDate"`
- ClosedReason string `json:"closedReason"`
- ActivatedDate *common.Iso8601Time `json:"activatedDate"`
- ToBug int `json:"toBug"`
- ChildStories string `json:"childStories"`
- LinkStories string `json:"linkStories"`
- LinkRequirements string `json:"linkRequirements"`
- DuplicateStory int64 `json:"duplicateStory"`
- Version int `json:"version"`
- StoryChanged string `json:"storyChanged"`
- FeedbackBy string `json:"feedbackBy"`
- NotifyEmail string `json:"notifyEmail"`
- URChanged string `json:"URChanged"`
- Deleted bool `json:"deleted"`
- PriOrder string `json:"priOrder"`
- PlanTitle string `json:"planTitle"`
- ProductStatus string `json:"productStatus"`
+ ID int64 `json:"id"`
+ Vision string `json:"vision"`
+ Parent int64 `json:"parent"`
+ Product int64 `json:"product"`
+ Branch int `json:"branch"`
+ Module int `json:"module"`
+ Plan string `json:"plan"`
+ Source string `json:"source"`
+ SourceNote string `json:"sourceNote"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Type string `json:"type"`
+ Category string `json:"category"`
+ Pri int `json:"pri"`
+ Estimate float64 `json:"estimate"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Stage string `json:"stage"`
+ Mailto []interface{} `json:"mailto"`
+ Lib int `json:"lib"`
+ FromStory int64 `json:"fromStory"`
+ FromVersion int `json:"fromVersion"`
+ OpenedBy *ApiAccount `json:"openedBy"`
+ OpenedDate *common.Iso8601Time `json:"openedDate"`
+ AssignedTo *ApiAccount `json:"assignedTo"`
+ AssignedDate *common.Iso8601Time `json:"assignedDate"`
+ ApprovedDate *common.Iso8601Time `json:"approvedDate"`
+ LastEditedBy *ApiAccount `json:"lastEditedBy"`
+ LastEditedDate *common.Iso8601Time `json:"lastEditedDate"`
+ ChangedBy string `json:"changedBy"`
+ ChangedDate *common.Iso8601Time `json:"changedDate"`
+ ReviewedBy *ApiAccount `json:"reviewedBy"`
+ ReviewedDate *common.Iso8601Time `json:"reviewedDate"`
+ ClosedBy *ApiAccount `json:"closedBy"`
+ ClosedDate *common.Iso8601Time `json:"closedDate"`
+ ClosedReason string `json:"closedReason"`
+ ActivatedDate *common.Iso8601Time `json:"activatedDate"`
+ ToBug int `json:"toBug"`
+ ChildStories string `json:"childStories"`
+ LinkStories string `json:"linkStories"`
+ LinkRequirements string `json:"linkRequirements"`
+ DuplicateStory int64 `json:"duplicateStory"`
+ Version int `json:"version"`
+ StoryChanged string `json:"storyChanged"`
+ FeedbackBy string `json:"feedbackBy"`
+ NotifyEmail string `json:"notifyEmail"`
+ URChanged string `json:"URChanged"`
+ Deleted bool `json:"deleted"`
+ PriOrder *common.StringFloat64 `json:"priOrder"`
+ PlanTitle string `json:"planTitle"`
+ ProductStatus string `json:"productStatus"`
}
type ZentaoStory struct {
diff --git a/backend/plugins/zentao/models/task.go b/backend/plugins/zentao/models/task.go
index 8bbc5fe..494d64a 100644
--- a/backend/plugins/zentao/models/task.go
+++ b/backend/plugins/zentao/models/task.go
@@ -83,12 +83,12 @@
} `json:"latestStoryVersion"`
StoryStatus interface {
} `json:"storyStatus"`
- AssignedToRealName string `json:"assignedToRealName"`
- PriOrder string `json:"priOrder"`
- Children []*ZentaoTaskRes `json:"children"`
- Delay int `json:"delay"`
- NeedConfirm bool `json:"needConfirm"`
- Progress float64 `json:"progress"`
+ AssignedToRealName string `json:"assignedToRealName"`
+ PriOrder *common.StringFloat64 `json:"priOrder"`
+ Children []*ZentaoTaskRes `json:"children"`
+ Delay int `json:"delay"`
+ NeedConfirm bool `json:"needConfirm"`
+ Progress float64 `json:"progress"`
}
func (zentaoTaskRes ZentaoTaskRes) ToJsonRawMessage() (json.RawMessage, error) {
diff --git a/backend/plugins/zentao/tasks/story_extractor.go b/backend/plugins/zentao/tasks/story_extractor.go
index 7f94c4f..1d00dcf 100644
--- a/backend/plugins/zentao/tasks/story_extractor.go
+++ b/backend/plugins/zentao/tasks/story_extractor.go
@@ -120,7 +120,7 @@
NotifyEmail: res.NotifyEmail,
URChanged: res.URChanged,
Deleted: res.Deleted,
- PriOrder: res.PriOrder,
+ PriOrder: res.PriOrder.String(),
PlanTitle: res.PlanTitle,
Url: row.Url,
}
diff --git a/backend/plugins/zentao/tasks/task_extractor.go b/backend/plugins/zentao/tasks/task_extractor.go
index d18701c..422e5ee 100644
--- a/backend/plugins/zentao/tasks/task_extractor.go
+++ b/backend/plugins/zentao/tasks/task_extractor.go
@@ -146,7 +146,7 @@
StoryTitle: res.StoryTitle,
LatestStoryVersion: 0,
AssignedToRealName: res.AssignedToRealName,
- PriOrder: res.PriOrder,
+ PriOrder: res.PriOrder.String(),
NeedConfirm: res.NeedConfirm,
Progress: res.Progress,
Url: url,
diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index a4894cf..e04e9e6 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -166,7 +166,7 @@
}
// Start the server
- err = router.Run(fmt.Sprintf("0.0.0.0:%d", portNum))
+ err = router.Run(fmt.Sprintf(":%d", portNum))
if err != nil {
panic(err)
}
diff --git a/backend/server/api/middlewares.go b/backend/server/api/middlewares.go
index 9e274c2..32ec5e8 100644
--- a/backend/server/api/middlewares.go
+++ b/backend/server/api/middlewares.go
@@ -20,6 +20,7 @@
import (
"encoding/base64"
"fmt"
+ "github.com/apache/incubator-devlake/core/log"
"net/http"
"regexp"
"strings"
@@ -95,11 +96,13 @@
}
}
+type apiBody struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+}
+
func RestAuthentication(router *gin.Engine, basicRes context.BasicRes) gin.HandlerFunc {
- type ApiBody struct {
- Success bool `json:"success"`
- Message string `json:"message"`
- }
+
db := basicRes.GetDal()
logger := basicRes.GetLogger()
if db == nil {
@@ -115,88 +118,97 @@
return
}
path = strings.TrimPrefix(path, "/rest")
-
authHeader := c.GetHeader("Authorization")
- if authHeader == "" {
+ ok := CheckAuthorizationHeader(c, logger, db, apiKeyHelper, authHeader, path)
+ if !ok {
+ return
+ } else {
+ router.HandleContext(c)
c.Abort()
- c.JSON(http.StatusUnauthorized, &ApiBody{
- Success: false,
- Message: "token is missing",
- })
return
}
- apiKeyStr := strings.TrimPrefix(authHeader, "Bearer ")
- if apiKeyStr == authHeader || apiKeyStr == "" {
- c.Abort()
- c.JSON(http.StatusUnauthorized, &ApiBody{
- Success: false,
- Message: "token is not present or malformed",
- })
- return
- }
+ }
+}
- hashedApiKey, err := apiKeyHelper.DigestToken(apiKeyStr)
- if err != nil {
- logger.Error(err, "DigestToken")
- c.Abort()
- c.JSON(http.StatusInternalServerError, &ApiBody{
+func CheckAuthorizationHeader(c *gin.Context, logger log.Logger, db dal.Dal, apiKeyHelper *apikeyhelper.ApiKeyHelper, authHeader, path string) bool {
+ if authHeader == "" {
+ c.Abort()
+ c.JSON(http.StatusUnauthorized, &apiBody{
+ Success: false,
+ Message: "token is missing",
+ })
+ return false
+ }
+ apiKeyStr := strings.TrimPrefix(authHeader, "Bearer ")
+ if apiKeyStr == authHeader || apiKeyStr == "" {
+ c.Abort()
+ c.JSON(http.StatusUnauthorized, &apiBody{
+ Success: false,
+ Message: "token is not present or malformed",
+ })
+ return false
+ }
+
+ hashedApiKey, err := apiKeyHelper.DigestToken(apiKeyStr)
+ if err != nil {
+ logger.Error(err, "DigestToken")
+ c.Abort()
+ c.JSON(http.StatusInternalServerError, &apiBody{
+ Success: false,
+ Message: err.Error(),
+ })
+ return false
+ }
+
+ apiKey, err := apiKeyHelper.GetApiKey(nil, dal.Where("api_key = ?", hashedApiKey))
+ if err != nil {
+ c.Abort()
+ if db.IsErrorNotFound(err) {
+ c.JSON(http.StatusForbidden, &apiBody{
+ Success: false,
+ Message: "api key is invalid",
+ })
+ } else {
+ logger.Error(err, "query api key from db")
+ c.JSON(http.StatusInternalServerError, &apiBody{
Success: false,
Message: err.Error(),
})
- return
}
-
- apiKey, err := apiKeyHelper.GetApiKey(nil, dal.Where("api_key = ?", hashedApiKey))
- if err != nil {
- c.Abort()
- if db.IsErrorNotFound(err) {
- c.JSON(http.StatusForbidden, &ApiBody{
- Success: false,
- Message: "api key is invalid",
- })
- } else {
- logger.Error(err, "query api key from db")
- c.JSON(http.StatusInternalServerError, &ApiBody{
- Success: false,
- Message: err.Error(),
- })
- }
- return
- }
-
- if apiKey.ExpiredAt != nil && time.Until(*apiKey.ExpiredAt) < 0 {
- c.Abort()
- c.JSON(http.StatusForbidden, &ApiBody{
- Success: false,
- Message: "api key has expired",
- })
- return
- }
- matched, matchErr := regexp.MatchString(apiKey.AllowedPath, path)
- if matchErr != nil {
- logger.Error(err, "regexp match path error")
- c.Abort()
- c.JSON(http.StatusInternalServerError, &ApiBody{
- Success: false,
- Message: matchErr.Error(),
- })
- return
- }
- if !matched {
- c.JSON(http.StatusForbidden, &ApiBody{
- Success: false,
- Message: "path doesn't match api key's scope",
- })
- return
- }
-
- logger.Info("redirect path: %s to: %s", c.Request.URL.Path, path)
- c.Request.URL.Path = path
- c.Set(common.USER, &common.User{
- Name: apiKey.Creator.Creator,
- Email: apiKey.Creator.CreatorEmail,
- })
- router.HandleContext(c)
- c.Abort()
+ return false
}
+
+ if apiKey.ExpiredAt != nil && time.Until(*apiKey.ExpiredAt) < 0 {
+ c.Abort()
+ c.JSON(http.StatusForbidden, &apiBody{
+ Success: false,
+ Message: "api key has expired",
+ })
+ return false
+ }
+ matched, matchErr := regexp.MatchString(apiKey.AllowedPath, path)
+ if matchErr != nil {
+ logger.Error(err, "regexp match path error")
+ c.Abort()
+ c.JSON(http.StatusInternalServerError, &apiBody{
+ Success: false,
+ Message: matchErr.Error(),
+ })
+ return false
+ }
+ if !matched {
+ c.JSON(http.StatusForbidden, &apiBody{
+ Success: false,
+ Message: "path doesn't match api key's scope",
+ })
+ return false
+ }
+
+ logger.Info("redirect path: %s to: %s", c.Request.URL.Path, path)
+ c.Request.URL.Path = path
+ c.Set(common.USER, &common.User{
+ Name: apiKey.Creator.Creator,
+ Email: apiKey.Creator.CreatorEmail,
+ })
+ return true
}
diff --git a/backend/server/services/blueprint.go b/backend/server/services/blueprint.go
index fa28508..9a8407e 100644
--- a/backend/server/services/blueprint.go
+++ b/backend/server/services/blueprint.go
@@ -68,22 +68,8 @@
// CreateBlueprint accepts a Blueprint instance and insert it to database
func CreateBlueprint(blueprint *models.Blueprint) errors.Error {
- err := validateBlueprintAndMakePlan(blueprint)
- if err != nil {
- return err
- }
- err = bpManager.SaveDbBlueprint(blueprint)
- if err != nil {
- return err
- }
- if err := SanitizeBlueprint(blueprint); err != nil {
- return errors.Convert(err)
- }
- err = ReloadBlueprints(cronManager)
- if err != nil {
- return errors.Internal.Wrap(err, "error reloading blueprints")
- }
- return nil
+ _, err := saveBlueprint(blueprint)
+ return err
}
// GetBlueprints returns a paginated list of Blueprints based on `query`
@@ -204,9 +190,9 @@
}
// reload schedule
- err = ReloadBlueprints(cronManager)
+ err = reloadBlueprint(blueprint)
if err != nil {
- return nil, errors.Internal.Wrap(err, "error reloading blueprints")
+ return nil, err
}
// done
return blueprint, nil
@@ -260,14 +246,10 @@
}
var blueprintReloadLock sync.Mutex
+var bpCronIdMap map[uint64]cron.EntryID
-// ReloadBlueprints FIXME ...
-func ReloadBlueprints(c *cron.Cron) (err errors.Error) {
- // preventing concurrent reloads. It would be better to use Table Lock , however, it requires massive refactor
- // like the `bpManager` must accept transaction. Use mutex as a temporary fix.
- blueprintReloadLock.Lock()
- defer blueprintReloadLock.Unlock()
-
+// ReloadBlueprints reloades cronjobs based on blueprints
+func ReloadBlueprints() (err errors.Error) {
enable := true
isManual := false
blueprints, _, err := bpManager.GetDbBlueprints(&services.GetBlueprintQuery{
@@ -277,27 +259,48 @@
if err != nil {
return err
}
- for _, e := range c.Entries() {
- c.Remove(e.ID)
+ for _, e := range cronManager.Entries() {
+ cronManager.Remove(e.ID)
}
- c.Stop()
+ cronManager.Stop()
+ bpCronIdMap = make(map[uint64]cron.EntryID, len(blueprints))
for _, blueprint := range blueprints {
- blueprintLog.Info("Add blueprint id:[%d] cronConfg[%s] to cron job", blueprint.ID, blueprint.CronConfig)
- blueprintJob := &BlueprintJob{
- Blueprint: blueprint,
- }
- if _, err := c.AddJob(blueprint.CronConfig, blueprintJob); err != nil {
- blueprintLog.Error(err, failToCreateCronJob)
- return errors.Default.Wrap(err, "created cron job failed")
+ err := reloadBlueprint(blueprint)
+ if err != nil {
+ return err
}
}
if len(blueprints) > 0 {
- c.Start()
+ cronManager.Start()
}
logger.Info("total %d blueprints were scheduled", len(blueprints))
return nil
}
+func reloadBlueprint(blueprint *models.Blueprint) errors.Error {
+ // preventing concurrent reloads. It would be better to use Table Lock , however, it requires massive refactor
+ // like the `bpManager` must accept transaction. Use mutex as a temporary fix.
+ blueprintReloadLock.Lock()
+ defer blueprintReloadLock.Unlock()
+
+ cronId, scheduled := bpCronIdMap[blueprint.ID]
+ if scheduled {
+ cronManager.Remove(cronId)
+ delete(bpCronIdMap, blueprint.ID)
+ logger.Info("removed blueprint %d from cronjobs, cron id: %v", blueprint.ID, cronId)
+ }
+ if blueprint.Enable && !blueprint.IsManual {
+ if cronId, err := cronManager.AddJob(blueprint.CronConfig, &BlueprintJob{blueprint}); err != nil {
+ blueprintLog.Error(err, failToCreateCronJob)
+ return errors.Default.Wrap(err, "created cron job failed")
+ } else {
+ bpCronIdMap[blueprint.ID] = cronId
+ logger.Info("added blueprint %d to cronjobs, cron id: %v, cron config: %s", blueprint.ID, cronId, blueprint.CronConfig)
+ }
+ }
+ return nil
+}
+
func createPipelineByBlueprint(blueprint *models.Blueprint, syncPolicy *models.SyncPolicy) (*models.Pipeline, errors.Error) {
var plan models.PipelinePlan
var err errors.Error
diff --git a/backend/server/services/pipeline.go b/backend/server/services/pipeline.go
index 1783c94..2a1ac8c 100644
--- a/backend/server/services/pipeline.go
+++ b/backend/server/services/pipeline.go
@@ -103,7 +103,7 @@
panic(err)
}
- err = ReloadBlueprints(cronManager)
+ err = ReloadBlueprints()
if err != nil {
panic(err)
}