blob: bbe5cf7b5328f46cae08369e5b0664b82a792cb3 [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 service
import (
"fmt"
"strings"
"github.com/apache/dubbo-admin/pkg/common/bizerror"
"github.com/apache/dubbo-admin/pkg/common/constants"
consolectx "github.com/apache/dubbo-admin/pkg/console/context"
"github.com/apache/dubbo-admin/pkg/core/lock"
meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
"github.com/apache/dubbo-admin/pkg/core/versioning"
)
type RuleMutationOptions struct {
Author string
}
type RuleRef struct {
Kind coremodel.ResourceKind
Mesh string
Name string
}
func ruleVersioning(ctx consolectx.Context) *versioning.Service {
if ctx == nil {
return nil
}
return ctx.RuleVersioning()
}
func requiredRuleVersioning(ctx consolectx.Context) (*versioning.Service, error) {
svc := ruleVersioning(ctx)
if svc == nil {
return nil, versioning.ErrVersionStoreError
}
return svc, nil
}
// getRuleIfExists reads the live ResourceManager state, not recorded history.
func getRuleIfExists(ctx consolectx.Context, ruleRef RuleRef) (coremodel.Resource, bool, error) {
key := coremodel.BuildResourceKey(ruleRef.Mesh, ruleRef.Name)
res, exists, err := ctx.ResourceManager().GetByKey(ruleRef.Kind, key)
if err != nil {
return nil, false, err
}
if !exists || res == nil {
return nil, false, nil
}
return res, true, nil
}
func getExistingRule(ctx consolectx.Context, ruleRef RuleRef) (coremodel.Resource, error) {
res, exists, err := getRuleIfExists(ctx, ruleRef)
if err != nil {
return nil, err
}
if !exists {
key := coremodel.BuildResourceKey(ruleRef.Mesh, ruleRef.Name)
return nil, fmt.Errorf("%s %s does not exist", ruleRef.Kind, key)
}
return res, nil
}
func appendRuleHistory(ctx consolectx.Context, res coremodel.Resource, op versioning.Operation, source versioning.Source, author, reason string, rolledBackFromVersionNo *int64) (*versioning.Version, error) {
svc, err := requiredRuleVersioning(ctx)
if err != nil {
return nil, err
}
return svc.Append(ctx.AppContext(), res, op, source, author, reason, rolledBackFromVersionNo)
}
func ensureBaselineHistory(ctx consolectx.Context, res coremodel.Resource) error {
svc, err := requiredRuleVersioning(ctx)
if err != nil {
return err
}
if res == nil {
return nil
}
hasHistory, err := svc.HasHistory(res.ResourceKind(), res.ResourceMesh(), res.ResourceMeta().Name)
if err != nil {
return err
}
if hasHistory {
return nil
}
if _, err := svc.Append(ctx.AppContext(), res, versioning.OperationCreate, versioning.SourceBootstrap, "system:baseline", "import existing rule before first edit", nil); err != nil {
return err
}
return nil
}
func withRuleLock(ctx consolectx.Context, ruleRef RuleRef, fn func() error) error {
lockMgr := ctx.LockManager()
if lockMgr == nil {
return fn()
}
lockKey := ruleLockKey(ruleRef)
return lockMgr.WithLock(ctx.AppContext(), lockKey, constants.DefaultLockTimeout, fn)
}
func ruleLockKey(ruleRef RuleRef) string {
switch ruleRef.Kind {
case meshresource.ConditionRouteKind:
return lock.BuildConditionRuleLockKey(ruleRef.Mesh, ruleRef.Name)
case meshresource.TagRouteKind:
return lock.BuildTagRouteLockKey(ruleRef.Mesh, ruleRef.Name)
case meshresource.DynamicConfigKind:
return lock.BuildConfiguratorRuleLockKey(ruleRef.Mesh, ruleRef.Name)
default:
return lock.BuildLockKey(ruleRef.Kind.ToString(), ruleRef.Mesh, ruleRef.Name)
}
}
func createRule(ctx consolectx.Context, res coremodel.Resource, opts RuleMutationOptions) error {
ruleRef := RuleRef{Kind: res.ResourceKind(), Mesh: res.ResourceMesh(), Name: res.ResourceMeta().Name}
return withRuleLock(ctx, ruleRef, func() error {
if _, err := appendRuleHistory(ctx, res, versioning.OperationCreate, versioning.SourceAdmin, opts.Author, "", nil); err != nil {
return err
}
if err := ctx.ResourceManager().Add(res); err != nil {
return err
}
return nil
})
}
func updateRule(ctx consolectx.Context, res coremodel.Resource, opts RuleMutationOptions) error {
ruleRef := RuleRef{Kind: res.ResourceKind(), Mesh: res.ResourceMesh(), Name: res.ResourceMeta().Name}
return withRuleLock(ctx, ruleRef, func() error {
existing, err := getExistingRule(ctx, ruleRef)
if err != nil {
return err
}
if err := ensureBaselineHistory(ctx, existing); err != nil {
return err
}
if _, err := appendRuleHistory(ctx, res, versioning.OperationUpdate, versioning.SourceAdmin, opts.Author, "", nil); err != nil {
return err
}
if err := ctx.ResourceManager().Update(res); err != nil {
return err
}
return nil
})
}
func deleteRule(ctx consolectx.Context, ruleRef RuleRef, opts RuleMutationOptions) error {
return withRuleLock(ctx, ruleRef, func() error {
resourceKey := coremodel.BuildResourceKey(ruleRef.Mesh, ruleRef.Name)
snapshot, exists, err := ctx.ResourceManager().GetByKey(ruleRef.Kind, resourceKey)
if err != nil {
return err
}
if !exists || snapshot == nil {
return nil
}
if err := ensureBaselineHistory(ctx, snapshot); err != nil {
return err
}
if _, err := appendRuleHistory(ctx, snapshot, versioning.OperationDelete, versioning.SourceAdmin, opts.Author, "", nil); err != nil {
return err
}
if err := ctx.ResourceManager().DeleteByKey(ruleRef.Kind, ruleRef.Mesh, resourceKey); err != nil {
return err
}
return nil
})
}
func ListRuleVersions(ctx consolectx.Context, ruleRef RuleRef) (*versioning.ListResult, error) {
svc := ruleVersioning(ctx)
if svc == nil {
return nil, versioning.ErrVersionStoreError
}
return svc.List(ruleRef.Kind, ruleRef.Mesh, ruleRef.Name)
}
func GetRuleVersion(ctx consolectx.Context, ruleRef RuleRef, versionNo int64) (*versioning.Version, error) {
svc := ruleVersioning(ctx)
if svc == nil {
return nil, versioning.ErrVersionStoreError
}
return svc.Get(ruleRef.Kind, ruleRef.Mesh, ruleRef.Name, versionNo)
}
func DiffRuleVersion(ctx consolectx.Context, ruleRef RuleRef, versionNo int64, against string) (*versioning.DiffResult, error) {
svc := ruleVersioning(ctx)
if svc == nil {
return nil, versioning.ErrVersionStoreError
}
if against != "" && against != "current" {
return svc.DiffHistoryVersions(ruleRef.Kind, ruleRef.Mesh, ruleRef.Name, versionNo, against)
}
left, err := svc.Get(ruleRef.Kind, ruleRef.Mesh, ruleRef.Name, versionNo)
if err != nil {
return nil, err
}
current, exists, err := getRuleIfExists(ctx, ruleRef)
if err != nil {
return nil, err
}
specJSON := versioning.DeleteSpecJSON
if exists {
_, specJSON, err = versioning.NormalizeResource(current)
if err != nil {
return nil, err
}
}
return &versioning.DiffResult{
Left: versioning.DiffSide{VersionNo: left.VersionNo, SpecJSON: left.SpecJSON},
Right: versioning.DiffSide{VersionNo: 0, SpecJSON: specJSON},
}, nil
}
// RollbackResult summarizes a rollback write for the API response.
type RollbackResult struct {
RolledBackFromVersionNo int64
VersionNo int64
Source string
}
func RollbackRuleVersion(ctx consolectx.Context, ruleRef RuleRef, targetVersionNo int64, reason string, author string) (*RollbackResult, error) {
svc := ruleVersioning(ctx)
if svc == nil {
return nil, versioning.ErrVersionStoreError
}
reason = strings.TrimSpace(reason)
if reason == "" {
return nil, bizerror.New(bizerror.InvalidArgument, "rollback reason is required")
}
var result *RollbackResult
err := withRuleLock(ctx, ruleRef, func() error {
target, err := svc.Get(ruleRef.Kind, ruleRef.Mesh, ruleRef.Name, targetVersionNo)
if err != nil {
return err
}
if target.Operation == versioning.OperationDelete {
return versioning.ErrRollbackToDelete
}
if target.Operation != versioning.OperationCreate && target.Operation != versioning.OperationUpdate {
return bizerror.New(bizerror.InvalidArgument, "only CREATE or UPDATE rule versions can be rolled back")
}
current, exists, err := getRuleIfExists(ctx, ruleRef)
if err != nil {
return err
}
if exists {
hash, _, err := versioning.NormalizeResource(current)
if err != nil {
return err
}
if hash == target.ContentHash {
return versioning.ErrRollbackToCurrent
}
}
res, err := versioning.ResourceFromSpecJSON(ruleRef.Kind, ruleRef.Mesh, ruleRef.Name, target.SpecJSON)
if err != nil {
return err
}
operation := versioning.OperationUpdate
if !exists {
operation = versioning.OperationCreate
}
rolledBackFromVersionNo := target.VersionNo
appended, err := appendRuleHistory(ctx, res, operation, versioning.SourceRollback, author, reason, &rolledBackFromVersionNo)
if err != nil {
return err
}
if err := ctx.ResourceManager().Upsert(res); err != nil {
return err
}
result = &RollbackResult{
RolledBackFromVersionNo: rolledBackFromVersionNo,
Source: string(versioning.SourceRollback),
VersionNo: appended.VersionNo,
}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}