From fa2a2bea18a148d13b0f27f1cf7784f5360334db Mon Sep 17 00:00:00 2001 From: Jian Qiu Date: Thu, 20 Jun 2024 07:54:20 +0800 Subject: [PATCH] fix: clone dashboard parameters (#20589) Co-authored-by: Qiu Jian --- cmd/climc/shell/yunionconf/parameters.go | 101 ++++++++++++ pkg/apis/yunionconf/consts.go | 6 + pkg/apis/yunionconf/input.go | 6 + pkg/yunionconf/models/parameters.go | 186 ++++++++++++++++++++--- 4 files changed, 281 insertions(+), 18 deletions(-) diff --git a/cmd/climc/shell/yunionconf/parameters.go b/cmd/climc/shell/yunionconf/parameters.go index 2a010881f6..0687ba746d 100644 --- a/cmd/climc/shell/yunionconf/parameters.go +++ b/cmd/climc/shell/yunionconf/parameters.go @@ -15,10 +15,15 @@ package yunionconf import ( + "fmt" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/printutils" "yunion.io/x/pkg/util/shellutils" + api "yunion.io/x/onecloud/pkg/apis/yunionconf" + "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/modules/identity" "yunion.io/x/onecloud/pkg/mcclient/modules/yunionconf" @@ -241,4 +246,100 @@ func init() { printObject(parameter) return nil }) + + type ParameterCloneOptions struct { + User string `help:"Clone parameter of specificated user id"` + Service string `help:"Clone parameter of specificated service id"` + NAME string `help:"The name of parameter"` + DestUser string `help:"destination user id of clone action"` + DestService string `help:"destination service id of clone action"` + DestName string `help:"destination parameter name of clone action, may be empty"` + } + R(&ParameterCloneOptions{}, "parameter-clone", "clone parameter", func(s *mcclient.ClientSession, args *ParameterCloneOptions) error { + input := api.ParameterCloneInput{} + + input.DestName = args.DestName + + if len(args.DestUser) > 0 { + input.DestNs = api.NAMESPACE_USER + input.DestNsId = args.DestUser + } else if len(args.DestService) > 0 { + input.DestNs = api.NAMESPACE_SERVICE + input.DestNsId = args.DestService + } else { + return errors.Wrap(httperrors.ErrMissingParameter, "either dest_user or dest_service must be specified") + } + params := jsonutils.Marshal(input).(*jsonutils.JSONDict) + + var parameter jsonutils.JSONObject + var err error + if len(args.User) > 0 { + parameter, err = yunionconf.Parameters.PerformActionInContext(s, args.NAME, "clone", params, &identity.UsersV3, args.User) + } else if len(args.Service) > 0 { + parameter, err = yunionconf.Parameters.PerformActionInContext(s, args.NAME, "clone", params, &identity.ServicesV3, args.Service) + } else { + parameter, err = yunionconf.Parameters.PerformAction(s, args.NAME, "clone", params) + } + + if err != nil { + return err + } + printObject(parameter) + return nil + }) + + type ParameterCloneDashboardOptions struct { + SCOPE string `help:"dashboard scope" choices:"system|domain|project"` + SRC string `help:"source user id"` + DST string `help:"destination user id"` + } + R(&ParameterCloneDashboardOptions{}, "parameter-clone-dashboard", "clone dashboard parameter", func(s *mcclient.ClientSession, args *ParameterCloneDashboardOptions) error { + cloneUserParams := func(srcUid, destUid, name string) (jsonutils.JSONObject, error) { + paramId, err := yunionconf.Parameters.GetIdInContext(s, name, nil, &identity.UsersV3, srcUid) + if err != nil { + return nil, errors.Wrapf(err, "GetByName %s", name) + } + input := api.ParameterCloneInput{ + DestNs: api.NAMESPACE_USER, + DestNsId: destUid, + } + params := jsonutils.Marshal(input).(*jsonutils.JSONDict) + return yunionconf.Parameters.PerformActionInContext(s, paramId, "clone", params, &identity.UsersV3, srcUid) + } + + rootName := fmt.Sprintf("dashboard_%s", args.SCOPE) + + confs := []struct { + Id string + Name string + }{} + + paramObj, err := yunionconf.Parameters.GetInContext(s, rootName, nil, &identity.UsersV3, args.SRC) + if err != nil { + return errors.Wrap(err, "GetParameter") + } + + err = paramObj.Unmarshal(&confs, "value") + if err != nil { + return errors.Wrap(err, "unmarshal value") + } + + for _, conf := range confs { + _, err := cloneUserParams(args.SRC, args.DST, conf.Id) + if err != nil { + return errors.Wrapf(err, "clone %s", conf.Id) + } + fmt.Println("cloned", conf.Id) + } + + // finally copy the root + _, err = cloneUserParams(args.SRC, args.DST, rootName) + if err != nil { + return errors.Wrapf(err, "clone %s", rootName) + } + fmt.Println("cloned", rootName) + + return nil + }) + } diff --git a/pkg/apis/yunionconf/consts.go b/pkg/apis/yunionconf/consts.go index 2ec8e36648..bc0ee4a9ba 100644 --- a/pkg/apis/yunionconf/consts.go +++ b/pkg/apis/yunionconf/consts.go @@ -24,3 +24,9 @@ const ( ANY_DOMAIN_ID = "[any_domain_id]" ANY_PROJECT_ID = "[any_project_id]" ) + +const ( + NAMESPACE_USER = "user" + NAMESPACE_SERVICE = "service" + NAMESPACE_BUG_REPORT = "bug-report" +) diff --git a/pkg/apis/yunionconf/input.go b/pkg/apis/yunionconf/input.go index c5ecaa6cf0..e7e4d41e90 100644 --- a/pkg/apis/yunionconf/input.go +++ b/pkg/apis/yunionconf/input.go @@ -44,6 +44,12 @@ type ParameterListInput struct { Name []string `json:"name"` } +type ParameterCloneInput struct { + DestNs string `json:"dest_ns"` + DestNsId string `json:"dest_ns_id"` + DestName string `json:"dest_name"` +} + type ScopedPolicyCreateInput struct { apis.InfrasResourceBaseCreateInput diff --git a/pkg/yunionconf/models/parameters.go b/pkg/yunionconf/models/parameters.go index 6ffd5ff813..3e04ced29c 100644 --- a/pkg/yunionconf/models/parameters.go +++ b/pkg/yunionconf/models/parameters.go @@ -28,18 +28,20 @@ import ( api "yunion.io/x/onecloud/pkg/apis/yunionconf" "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" "yunion.io/x/onecloud/pkg/cloudcommon/policy" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity" + "yunion.io/x/onecloud/pkg/util/logclient" "yunion.io/x/onecloud/pkg/yunionconf/options" ) const ( - NAMESPACE_USER = "user" - NAMESPACE_SERVICE = "service" - NAMESPACE_BUG_REPORT = "bug-report" + NAMESPACE_USER = api.NAMESPACE_USER + NAMESPACE_SERVICE = api.NAMESPACE_SERVICE + NAMESPACE_BUG_REPORT = api.NAMESPACE_BUG_REPORT ) type SParameterManager struct { @@ -84,8 +86,8 @@ func isAdminQuery(query jsonutils.JSONObject) bool { return false } -func getUserId(user string) (string, error) { - s := auth.GetAdminSession(context.Background(), options.Options.Region) +func getUserId(ctx context.Context, user string) (string, error) { + s := auth.GetAdminSession(ctx, options.Options.Region) userObj, err := modules.UsersV3.Get(s, user, nil) if err != nil { return "", err @@ -99,8 +101,8 @@ func getUserId(user string) (string, error) { return uid, nil } -func getServiceId(service string) (string, error) { - s := auth.GetAdminSession(context.Background(), options.Options.Region) +func getServiceId(ctx context.Context, service string) (string, error) { + s := auth.GetAdminSession(ctx, options.Options.Region) serviceObj, err := modules.ServicesV3.Get(s, service, nil) if err != nil { return "", err @@ -114,17 +116,17 @@ func getServiceId(service string) (string, error) { return uid, nil } -func getNamespaceInContext(userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (namespace string, namespaceId string, err error) { +func getNamespaceInContext(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (namespace string, namespaceId string, err error) { // 优先匹配上线文中的参数, /users//parameters /services//parameters if query != nil { if uid := jsonutils.GetAnyString(query, []string{"user", "user_id"}); len(uid) > 0 { - uid, err := getUserId(uid) + uid, err := getUserId(ctx, uid) if err != nil { return "", "", err } return NAMESPACE_USER, uid, nil } else if sid := jsonutils.GetAnyString(query, []string{"service", "service_id"}); len(sid) > 0 { - sid, err := getServiceId(sid) + sid, err := getServiceId(ctx, sid) if err != nil { return "", "", err } @@ -134,13 +136,13 @@ func getNamespaceInContext(userCred mcclient.TokenCredential, query jsonutils.JS // 匹配/parameters中的参数 if uid := jsonutils.GetAnyString(data, []string{"user", "user_id"}); len(uid) > 0 { - uid, err := getUserId(uid) + uid, err := getUserId(ctx, uid) if err != nil { return "", "", err } return NAMESPACE_USER, uid, nil } else if sid := jsonutils.GetAnyString(data, []string{"service", "service_id"}); len(sid) > 0 { - sid, err := getServiceId(sid) + sid, err := getServiceId(ctx, sid) if err != nil { return "", "", err } @@ -150,10 +152,10 @@ func getNamespaceInContext(userCred mcclient.TokenCredential, query jsonutils.JS } } -func getNamespace(userCred mcclient.TokenCredential, resource string, query jsonutils.JSONObject, data *jsonutils.JSONDict) (string, string, error) { +func getNamespace(ctx context.Context, userCred mcclient.TokenCredential, resource string, query jsonutils.JSONObject, data *jsonutils.JSONDict) (string, string, error) { var namespace, namespace_id string if policy.PolicyManager.Allow(rbacscope.ScopeSystem, userCred, consts.GetServiceType(), resource, policy.PolicyActionList).Result.IsAllow() { - if name, nameId, e := getNamespaceInContext(userCred, query, data); e != nil { + if name, nameId, e := getNamespaceInContext(ctx, userCred, query, data); e != nil { return "", "", e } else { namespace = name @@ -187,7 +189,7 @@ func (manager *SParameterManager) ValidateCreateData(ctx context.Context, userCr return nil, httperrors.NewUserNotFoundError("user not found") } - namespace, namespace_id, e := getNamespace(userCred, manager.KeywordPlural(), query, data) + namespace, namespace_id, e := getNamespace(ctx, userCred, manager.KeywordPlural(), query, data) if e != nil { return nil, e } @@ -253,13 +255,13 @@ func (manager *SParameterManager) ListItemFilter( if id := query.NamespaceId; len(id) > 0 { q = q.Equals("namespace_id", id) } else if id := query.ServiceId; len(id) > 0 { - if sid, err := getServiceId(id); err != nil { + if sid, err := getServiceId(ctx, id); err != nil { return q, err } else { q = q.Equals("namespace_id", sid).Equals("namespace", NAMESPACE_SERVICE) } } else if id := query.UserId; len(id) > 0 { - if uid, err := getUserId(id); err != nil { + if uid, err := getUserId(ctx, id); err != nil { return q, err } else { q = q.Equals("namespace_id", uid).Equals("namespace", NAMESPACE_USER) @@ -313,7 +315,7 @@ func (model *SParameter) ValidateUpdateData(ctx context.Context, userCred mcclie return nil, httperrors.NewUserNotFoundError("user not found") } - namespace, namespace_id, e := getNamespace(userCred, model.KeywordPlural(), query, data) + namespace, namespace_id, e := getNamespace(ctx, userCred, model.KeywordPlural(), query, data) if e != nil { return nil, e } @@ -411,3 +413,151 @@ func (manager *SParameterManager) DisableBugReport(ctx context.Context) error { bugReportEnable = nil return err } + +func (manager *SParameterManager) FetchParameters(nsType string, nsId string, name string) ([]SParameter, error) { + q := manager.Query() + q = q.Equals("namespace", nsType) + q = q.Equals("namespace_id", nsId) + if len(name) > 0 { + q = q.Equals("name", name) + } + params := make([]SParameter, 0) + err := db.FetchModelObjects(manager, q, ¶ms) + if err != nil { + return nil, errors.Wrap(err, "db.FetchModelObjects") + } + return params, nil +} + +func (parameter *SParameter) GetShortDesc(ctx context.Context) *jsonutils.JSONDict { + return jsonutils.Marshal(struct { + Id int64 + Name string + Namespace string + NamespaceId string + Value jsonutils.JSONObject + }{ + Id: parameter.Id, + Name: parameter.Name, + Namespace: parameter.Namespace, + NamespaceId: parameter.NamespaceId, + Value: parameter.Value, + }).(*jsonutils.JSONDict) +} + +func (parameter *SParameter) PerformClone( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input *api.ParameterCloneInput, +) (jsonutils.JSONObject, error) { + if len(input.DestName) == 0 { + input.DestName = parameter.Name + } + var nsType string + var nsId string + switch input.DestNs { + case "user", "users": + uid, err := getUserId(ctx, input.DestNsId) + if err != nil { + return nil, errors.Wrapf(err, "getDestUserId %s", input.DestNsId) + } + nsType = NAMESPACE_USER + nsId = uid + case "service", "services": + sid, err := getServiceId(ctx, input.DestNsId) + if err != nil { + return nil, errors.Wrapf(err, "getDestServiceId %s", input.DestNsId) + } + nsType = NAMESPACE_SERVICE + nsId = sid + default: + return nil, errors.Wrapf(errors.ErrNotSupported, "unsupported namespace %s/%s", input.DestNs, input.DestNsId) + } + + lockman.LockClass(ctx, ParameterManager, nsId) + defer lockman.ReleaseClass(ctx, ParameterManager, nsId) + + destParams, err := ParameterManager.FetchParameters(nsType, nsId, input.DestName) + if err != nil { + return nil, errors.Wrap(err, "FetchParameters") + } + switch len(destParams) { + case 0: + // create it + if !policy.PolicyManager.Allow(rbacscope.ScopeSystem, userCred, consts.GetServiceType(), ParameterManager.KeywordPlural(), policy.PolicyActionCreate).Result.IsAllow() { + return nil, httperrors.ErrNotSufficientPrivilege + } + newParam := SParameter{} + newParam.SetModelManager(ParameterManager, &newParam) + newParam.Namespace = nsType + newParam.NamespaceId = nsId + newParam.Name = input.DestName + newParam.Value = parameter.Value + newParam.CreatedBy = userCred.GetUserId() + newParam.UpdatedBy = userCred.GetUserId() + + err := ParameterManager.TableSpec().Insert(ctx, &newParam) + if err != nil { + return nil, errors.Wrap(err, "Insert") + } + logclient.AddActionLogWithContext(ctx, &newParam, logclient.ACT_CREATE, newParam.GetShortDesc(ctx), userCred, true) + return jsonutils.Marshal(&newParam), nil + case 1: + // update it + if !policy.PolicyManager.Allow(rbacscope.ScopeSystem, userCred, consts.GetServiceType(), ParameterManager.KeywordPlural(), policy.PolicyActionUpdate).Result.IsAllow() { + return nil, httperrors.ErrNotSufficientPrivilege + } + destParam := destParams[0] + lockman.LockObject(ctx, &destParam) + defer lockman.ReleaseObject(ctx, &destParam) + + var newValue jsonutils.JSONObject + if parameter.Value != nil { + switch srcVal := parameter.Value.(type) { + case *jsonutils.JSONDict: + if destParam.Value == nil { + newValue = srcVal + } else if destDict, ok := destParam.Value.(*jsonutils.JSONDict); ok { + dest := jsonutils.NewDict() + dest.Update(destDict) + dest.Update(srcVal) + newValue = dest + } else { + return nil, errors.Wrap(httperrors.ErrInvalidFormat, "cannot clone dictionary value to other type") + } + case *jsonutils.JSONArray: + if destParam.Value == nil { + newValue = srcVal + } else if destArray, ok := destParam.Value.(*jsonutils.JSONArray); ok { + dest := destArray.Copy() + srcObjs, _ := srcVal.GetArray() + dest.Add(srcObjs...) + newValue = dest + } else { + return nil, errors.Wrap(httperrors.ErrInvalidFormat, "cannot clone array value to other type") + } + default: + newValue = srcVal + } + } else { + // null operation + return nil, nil + } + + diff, err := db.Update(&destParam, func() error { + destParam.Value = newValue + destParam.UpdatedBy = userCred.GetUserId() + return nil + }) + if err != nil { + logclient.AddActionLogWithContext(ctx, &destParam, logclient.ACT_UPDATE, diff, userCred, false) + return nil, errors.Wrap(err, "update") + } + logclient.AddActionLogWithContext(ctx, &destParam, logclient.ACT_UPDATE, diff, userCred, true) + return jsonutils.Marshal(parameter), nil + default: + // error? + return nil, errors.Wrapf(httperrors.ErrInternalError, "duplicate dest?") + } +}