blob: 711a5e267ef327f8bbf8a07848539ec3927f5730 [file]
/*
* 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 handler
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/apache/dubbo-admin/pkg/common/bizerror"
consolectx "github.com/apache/dubbo-admin/pkg/console/context"
"github.com/apache/dubbo-admin/pkg/console/model"
"github.com/apache/dubbo-admin/pkg/console/service"
"github.com/apache/dubbo-admin/pkg/console/util"
coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
"github.com/apache/dubbo-admin/pkg/core/versioning"
)
const maxRuleVersionReasonLength = 1024
func ListRuleVersions(cs consolectx.Context, kind coremodel.ResourceKind) gin.HandlerFunc {
return func(c *gin.Context) {
if !ensureVersioningAvailable(c, cs) {
return
}
resp, err := service.ListRuleVersions(cs, ruleRef(c, kind))
if writeVersioningError(c, err) {
return
}
c.JSON(http.StatusOK, model.NewSuccessResp(toRuleVersionListResp(resp)))
}
}
func GetRuleVersion(cs consolectx.Context, kind coremodel.ResourceKind) gin.HandlerFunc {
return func(c *gin.Context) {
if !ensureVersioningAvailable(c, cs) {
return
}
versionNo, ok := parseVersionNo(c)
if !ok {
return
}
resp, err := service.GetRuleVersion(cs, ruleRef(c, kind), versionNo)
if writeVersioningError(c, err) {
return
}
c.JSON(http.StatusOK, model.NewSuccessResp(toRuleVersionResp(resp)))
}
}
func DiffRuleVersion(cs consolectx.Context, kind coremodel.ResourceKind) gin.HandlerFunc {
return func(c *gin.Context) {
if !ensureVersioningAvailable(c, cs) {
return
}
versionNo, ok := parseVersionNo(c)
if !ok {
return
}
resp, err := service.DiffRuleVersion(cs, ruleRef(c, kind), versionNo, c.Query("against"))
if writeVersioningError(c, err) {
return
}
c.JSON(http.StatusOK, model.NewSuccessResp(toRuleVersionDiffResp(resp)))
}
}
func RollbackRuleVersion(cs consolectx.Context, kind coremodel.ResourceKind) gin.HandlerFunc {
return func(c *gin.Context) {
if !ensureVersioningAvailable(c, cs) {
return
}
versionNo, ok := parseVersionNo(c)
if !ok {
return
}
req := model.RollbackRuleVersionReq{}
if err := c.ShouldBindJSON(&req); err != nil {
writeVersioningInvalidArgument(c, err.Error())
return
}
if !validateRuleVersionReasonLength(c, req.Reason) {
return
}
resp, err := service.RollbackRuleVersion(cs, ruleRef(c, kind), versionNo, req.Reason, currentUser(c))
if writeVersioningError(c, err) {
return
}
c.JSON(http.StatusOK, model.NewSuccessResp(toRollbackRuleVersionResp(resp)))
}
}
func validateRuleVersionReasonLength(c *gin.Context, reason string) bool {
if len(strings.TrimSpace(reason)) <= maxRuleVersionReasonLength {
return true
}
writeVersioningInvalidArgument(c, "reason must be at most 1024 characters")
return false
}
func mutationOptions(c *gin.Context) service.RuleMutationOptions {
return service.RuleMutationOptions{Author: currentUser(c)}
}
func ruleRef(c *gin.Context, kind coremodel.ResourceKind) service.RuleRef {
return service.RuleRef{Kind: kind, Mesh: c.Query("mesh"), Name: c.Param("ruleName")}
}
func parseVersionNo(c *gin.Context) (int64, bool) {
versionNo, err := parsePositiveInt64(c.Param("versionNo"))
if err != nil {
writeVersioningInvalidArgument(c, "versionNo must be a positive decimal string")
return 0, false
}
return versionNo, true
}
func parsePositiveInt64(raw string) (int64, error) {
if raw == "" {
return 0, fmt.Errorf("empty value")
}
for i := range raw {
if raw[i] < '0' || raw[i] > '9' {
return 0, fmt.Errorf("invalid decimal value")
}
}
if len(raw) > 1 && raw[0] == '0' {
return 0, fmt.Errorf("invalid leading zero")
}
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return 0, err
}
if value <= 0 {
return 0, fmt.Errorf("value must be positive")
}
return value, nil
}
func currentUser(c *gin.Context) string {
session := sessions.Default(c)
if user, ok := session.Get("user").(string); ok && strings.TrimSpace(user) != "" {
return user
}
return "system:unknown"
}
func ensureVersioningAvailable(c *gin.Context, cs consolectx.Context) bool {
if cs.RuleVersioning() != nil {
return true
}
util.HandleServiceError(c, bizerror.New(bizerror.InternalError, "rule history service is unavailable"))
return false
}
func writeVersioningError(c *gin.Context, err error) bool {
if err == nil {
return false
}
util.HandleServiceError(c, versioningServiceError(err))
return true
}
func versioningServiceError(err error) error {
var bizErr bizerror.Error
switch {
case errors.Is(err, versioning.ErrVersionNotFound):
return bizerror.New(bizerror.NotFoundError, err.Error())
case errors.Is(err, versioning.ErrRollbackToDelete), errors.Is(err, versioning.ErrRollbackToCurrent):
return bizerror.New(bizerror.InvalidArgument, err.Error())
case errors.As(err, &bizErr):
return bizErr
default:
return err
}
}
func toRuleVersionResp(version *versioning.Version) model.RuleVersionResp {
return model.RuleVersionResp{
RuleKind: version.RuleKind,
Mesh: version.Mesh,
ResourceKey: version.ResourceKey,
RuleName: version.RuleName,
VersionNo: version.VersionNo,
ContentHash: version.ContentHash,
SpecJSON: version.SpecJSON,
Source: version.Source,
Operation: version.Operation,
Author: version.Author,
Reason: version.Reason,
RolledBackFromVersionNo: version.RolledBackFromVersionNo,
CreatedAt: version.CreatedAt,
RecordedAt: version.RecordedAt,
IsLatestRecorded: version.IsLatestRecorded,
}
}
func toRuleVersionListResp(result *versioning.ListResult) model.RuleVersionListResp {
items := make([]model.RuleVersionResp, 0, len(result.Items))
for i := range result.Items {
items = append(items, toRuleVersionResp(&result.Items[i]))
}
return model.RuleVersionListResp{
Items: items,
Total: result.Total,
LatestRecordedVersionNo: result.LatestRecordedVersionNo,
LatestRecordedDeleted: result.LatestRecordedDeleted,
}
}
func toRuleVersionDiffResp(result *versioning.DiffResult) model.RuleVersionDiffResp {
return model.RuleVersionDiffResp{
Left: toRuleVersionDiffSideResp(result.Left),
Right: toRuleVersionDiffSideResp(result.Right),
}
}
func toRuleVersionDiffSideResp(side versioning.DiffSide) model.RuleVersionDiffSideResp {
return model.RuleVersionDiffSideResp{
VersionNo: side.VersionNo,
SpecJSON: side.SpecJSON,
}
}
func toRollbackRuleVersionResp(result *service.RollbackResult) model.RollbackRuleVersionResp {
return model.RollbackRuleVersionResp{
RolledBackFromVersionNo: result.RolledBackFromVersionNo,
VersionNo: result.VersionNo,
Source: result.Source,
}
}
func writeVersioningInvalidArgument(c *gin.Context, message string) {
c.JSON(http.StatusBadRequest, model.NewBizErrorResp(bizerror.New(bizerror.InvalidArgument, message)))
}