fix: task params nil params panic (#25699)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2026-09-18 11:57:35 +08:00
committed by GitHub
parent d108efd764
commit 472b0ae4af
8 changed files with 228 additions and 56 deletions

2
go.mod
View File

@@ -122,7 +122,7 @@ require (
yunion.io/x/jsonutils v1.0.1-0.20260917025845-3108cd9a32ea yunion.io/x/jsonutils v1.0.1-0.20260917025845-3108cd9a32ea
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91 yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900
yunion.io/x/pkg v1.10.4-0.20260916163305-b7743fa5e758 yunion.io/x/pkg v1.10.4-0.20260918012554-27cd9d2e093b
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1
yunion.io/x/sqlchemy v1.1.3-0.20260917035232-d682485b3a12 yunion.io/x/sqlchemy v1.1.3-0.20260917035232-d682485b3a12
yunion.io/x/structarg v0.0.0-20260917033311-96c7653334ac yunion.io/x/structarg v0.0.0-20260917033311-96c7653334ac

3
go.sum
View File

@@ -1989,8 +1989,9 @@ yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 h1:Hu/4ERvoWaN6aiFs4h4/yvVB
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900/go.mod h1:0vLkNEhlmA64HViPBAnSTUMrx5QP1CLsxXmxDKQ80tc= yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900/go.mod h1:0vLkNEhlmA64HViPBAnSTUMrx5QP1CLsxXmxDKQ80tc=
yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v0.0.0-20200814072949-4f1b541857d6/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/pkg v0.0.0-20200814072949-4f1b541857d6/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v1.10.4-0.20260916163305-b7743fa5e758 h1:3p/GCOPvDfvlSfovOh9GKxsSa7PPqp//DUjGRX8Wyus=
yunion.io/x/pkg v1.10.4-0.20260916163305-b7743fa5e758/go.mod h1:Crp6aUYB+Yq16DFjZPHhHq7ha5HL2DfyPvzCo2LJ+LU= yunion.io/x/pkg v1.10.4-0.20260916163305-b7743fa5e758/go.mod h1:Crp6aUYB+Yq16DFjZPHhHq7ha5HL2DfyPvzCo2LJ+LU=
yunion.io/x/pkg v1.10.4-0.20260918012554-27cd9d2e093b h1:XUJLpD48685DE8hwxEC+ms1F4T6/g2KKAfGuVn8rZeo=
yunion.io/x/pkg v1.10.4-0.20260918012554-27cd9d2e093b/go.mod h1:S8ITTpENaL3qSXi8Y4L6wVbVlP8XZ5BvABO10DFqF7M=
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 h1:1KJ3YYinydPHpDEQRXdr/T8SYcKZ5Er+m489H+PnaQ4= yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 h1:1KJ3YYinydPHpDEQRXdr/T8SYcKZ5Er+m489H+PnaQ4=
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo= yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo=
yunion.io/x/sqlchemy v1.1.3-0.20260917035232-d682485b3a12 h1:xU8Z/Zg+zc199Y58eB3fA8mvOHsA28UCDfd7OuK3QKA= yunion.io/x/sqlchemy v1.1.3-0.20260917035232-d682485b3a12 h1:xU8Z/Zg+zc199Y58eB3fA8mvOHsA28UCDfd7OuK3QKA=

View File

@@ -951,7 +951,22 @@ func (task *STask) ClearPendingUsage(index int) error {
} }
func (task *STask) GetParams() *jsonutils.JSONDict { func (task *STask) GetParams() *jsonutils.JSONDict {
return task.Params result := jsonutils.NewDict()
if task.Params == nil {
return result
}
copied := task.Params.DeepCopy()
copyParams, ok := copied.(*jsonutils.JSONDict)
if !ok || copyParams == nil {
return result
}
paramsJsonMap, _ := copyParams.GetMap()
for k, v := range paramsJsonMap {
if !strings.HasPrefix(k, "__") {
result.Set(k, v)
}
}
return result
} }
func (task *STask) GetUserCred() mcclient.TokenCredential { func (task *STask) GetUserCred() mcclient.TokenCredential {

View File

@@ -0,0 +1,144 @@
// Copyright 2019 Yunion
//
// 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 taskman
import (
"testing"
"yunion.io/x/jsonutils"
)
func TestSTaskGetParams(t *testing.T) {
newParams := func(pairs map[string]jsonutils.JSONObject) *jsonutils.JSONDict {
params := jsonutils.NewDict()
for k, v := range pairs {
params.Set(k, v)
}
return params
}
nested := jsonutils.NewDict()
nested.Set("ip", jsonutils.NewString("10.0.0.1"))
stages := jsonutils.NewArray(
jsonutils.NewDict(),
jsonutils.NewString("on_init"),
)
cases := []struct {
name string
params *jsonutils.JSONDict
want *jsonutils.JSONDict
}{
{
name: "nil_params",
params: nil,
want: jsonutils.NewDict(),
},
{
name: "empty_params",
params: jsonutils.NewDict(),
want: jsonutils.NewDict(),
},
{
name: "keep_public_keys",
params: newParams(map[string]jsonutils.JSONObject{
"parent_task_id": jsonutils.NewString("task-1"),
"auto_start": jsonutils.JSONTrue,
}),
want: newParams(map[string]jsonutils.JSONObject{
"parent_task_id": jsonutils.NewString("task-1"),
"auto_start": jsonutils.JSONTrue,
}),
},
{
name: "drop_internal_keys",
params: newParams(map[string]jsonutils.JSONObject{
"__stages": stages,
"__pending_usage__": jsonutils.NewDict(),
"__request_context": jsonutils.NewDict(),
"__parent_task_notifyurl": jsonutils.NewString("http://notify"),
}),
want: jsonutils.NewDict(),
},
{
name: "mixed_keys",
params: newParams(map[string]jsonutils.JSONObject{
"desc": nested,
"__stages": stages,
"_private": jsonutils.NewString("keep"),
"__": jsonutils.NewString("drop"),
"guest_id": jsonutils.NewString("g-1"),
}),
want: newParams(map[string]jsonutils.JSONObject{
"desc": nested,
"_private": jsonutils.NewString("keep"),
"guest_id": jsonutils.NewString("g-1"),
}),
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
task := &STask{}
task.Params = c.params
got := task.GetParams()
if got == nil {
t.Fatal("GetParams() returned nil, want empty dict")
}
if !got.Equals(c.want) {
t.Fatalf("GetParams() = %s, want %s", got, c.want)
}
})
}
}
func TestSTaskGetParamsDoesNotMutateOriginal(t *testing.T) {
orig := jsonutils.NewDict()
orig.Set("guest_id", jsonutils.NewString("g-1"))
orig.Set("__stages", jsonutils.NewArray(jsonutils.NewString("on_init")))
nested := jsonutils.NewDict()
nested.Set("ip", jsonutils.NewString("10.0.0.1"))
orig.Set("desc", nested)
task := &STask{STaskBase: STaskBase{Params: orig}}
got := task.GetParams()
got.Set("guest_id", jsonutils.NewString("g-2"))
got.Set("extra", jsonutils.JSONTrue)
desc, err := got.Get("desc")
if err != nil {
t.Fatalf("get desc: %v", err)
}
descDict, ok := desc.(*jsonutils.JSONDict)
if !ok {
t.Fatalf("desc type %T, want *jsonutils.JSONDict", desc)
}
descDict.Set("ip", jsonutils.NewString("10.0.0.2"))
if got.Contains("__stages") {
t.Fatal("GetParams() leaked internal key __stages")
}
guestId, _ := orig.GetString("guest_id")
if guestId != "g-1" {
t.Fatalf("original guest_id = %s, want g-1", guestId)
}
if !orig.Contains("__stages") {
t.Fatal("original __stages was removed")
}
origIp, _ := orig.GetString("desc", "ip")
if origIp != "10.0.0.1" {
t.Fatalf("original nested ip = %s, want 10.0.0.1", origIp)
}
}

View File

@@ -356,6 +356,7 @@ func (self *SBaremetalGuestDriver) RequestStartOnHost(ctx context.Context, guest
if params.Length() > 0 { if params.Length() > 0 {
config.Add(params, "params") config.Add(params, "params")
} }
log.Debugf("RequestStartOnHost config: %s", config.String())
headers := task.GetTaskRequestHeader() headers := task.GetTaskRequestHeader()
url := fmt.Sprintf("/baremetals/%s/servers/%s/start", host.Id, guest.Id) url := fmt.Sprintf("/baremetals/%s/servers/%s/start", host.Id, guest.Id)
_, err := host.BaremetalSyncRequest(ctx, "POST", url, headers, config) _, err := host.BaremetalSyncRequest(ctx, "POST", url, headers, config)

2
vendor/modules.txt vendored
View File

@@ -2788,7 +2788,7 @@ yunion.io/x/log/hooks
yunion.io/x/ovsdb/cli_util yunion.io/x/ovsdb/cli_util
yunion.io/x/ovsdb/schema/ovn_nb yunion.io/x/ovsdb/schema/ovn_nb
yunion.io/x/ovsdb/types yunion.io/x/ovsdb/types
# yunion.io/x/pkg v1.10.4-0.20260916163305-b7743fa5e758 # yunion.io/x/pkg v1.10.4-0.20260918012554-27cd9d2e093b
## explicit; go 1.18 ## explicit; go 1.18
yunion.io/x/pkg/appctx yunion.io/x/pkg/appctx
yunion.io/x/pkg/errors yunion.io/x/pkg/errors

View File

@@ -57,7 +57,7 @@ func expandAmbiguousPrefix(fields SStructFieldValueSet) SStructFieldValueSet {
if prefixed[idx] { if prefixed[idx] {
continue continue
} }
amPrefix, ok := fields[idx].Info.Tags[TAG_AMBIGUOUS_PREFIX] amPrefix, ok := fields[idx].Info.Tag(TAG_AMBIGUOUS_PREFIX)
if !ok { if !ok {
continue continue
} }
@@ -65,15 +65,25 @@ func expandAmbiguousPrefix(fields SStructFieldValueSet) SStructFieldValueSet {
if takenByOther(fields, expanded, indexes) { if takenByOther(fields, expanded, indexes) {
continue continue
} }
fields[idx].Info.Name = expanded info := fields[idx].Info
if depBy, ok := fields[idx].Info.Tags[TAG_DEPRECATED_BY]; ok { info.Name = expanded
fields[idx].Info.Tags[TAG_DEPRECATED_BY] = fmt.Sprintf("%s%s", amPrefix, depBy) _, newDepBy := info.tags[TAG_DEPRECATED_BY]
_, oldDepBy := info.tags[TAG_OLD_DEPRECATED_BY]
if newDepBy || oldDepBy {
// the tags may still be shared with other callers
info.copyTags()
} }
if depBy, ok := fields[idx].Info.Tags[TAG_OLD_DEPRECATED_BY]; ok { if newDepBy {
fields[idx].Info.Tags[TAG_OLD_DEPRECATED_BY] = fmt.Sprintf("%s%s", amPrefix, depBy) info.tags[TAG_DEPRECATED_BY] = fmt.Sprintf("%s%s", amPrefix, info.tags[TAG_DEPRECATED_BY])
}
if oldDepBy {
info.tags[TAG_OLD_DEPRECATED_BY] = fmt.Sprintf("%s%s", amPrefix, info.tags[TAG_OLD_DEPRECATED_BY])
}
if len(info.aliases) > 0 {
info.copyAliases()
for i := range info.aliases {
info.aliases[i] = fmt.Sprintf("%s%s", amPrefix, info.aliases[i])
} }
for i := range fields[idx].Info.Aliases {
fields[idx].Info.Aliases[i] = fmt.Sprintf("%s%s", amPrefix, fields[idx].Info.Aliases[i])
} }
prefixed[idx] = true prefixed[idx] = true
changed = true changed = true

View File

@@ -30,9 +30,10 @@ import (
// package. Do not construct a literal or modify the exported fields in an // package. Do not construct a literal or modify the exported fields in an
// unmanaged way // unmanaged way
// //
// The tags of a field are meant to be read through Tag and TagMap rather than // The tags of a field are read through Tag and TagMap. The map behind them
// off the Tags map, which is not handed out to stay writable by whoever read // is shared with the other callers of the fetch functions, which is what
// it; a caller may modify what TagMap returns. // makes reading a field several times cheap, so a caller may only modify what
// TagMap hands out, which is a copy of its own.
type SStructFieldInfo struct { type SStructFieldInfo struct {
// True if the field has json tag `json:"-"` // True if the field has json tag `json:"-"`
Ignore bool Ignore bool
@@ -72,40 +73,34 @@ type SStructFieldInfo struct {
// value as a json string // value as a json string
ForceString bool ForceString bool
// Tags holds the tags of the field keyed by tag name, a tag without a // tags holds the tags of the field keyed by tag name, a tag without a
// value being mapped to the empty string. Read it through Tag or // value being mapped to the empty string. The map is shared with the
// TagMap rather than off here. // other callers of the fetch functions, so it has to be copied with
Tags map[string]string // copyTags before being written to; read it through Tag or TagMap.
tags map[string]string
// Aliases are the other names the field is looked up by, taken from // aliases are the other names the field is looked up by, taken from
// the "alias" tag // the "alias" tag. Like tags it is shared, and has to be copied with
Aliases []string // copyAliases before being written to.
aliases []string
} }
func (s *SStructFieldInfo) updateTags(k, v string) { // copyTags takes a private copy of the tags, so that they can be written to
s.Tags[k] = v // without touching the ones this info was read out of.
} func (s *SStructFieldInfo) copyTags() {
tags := make(map[string]string, len(s.tags)+1)
func (s SStructFieldInfo) deepCopy() *SStructFieldInfo { for k, v := range s.tags {
scopy := SStructFieldInfo{
Ignore: s.Ignore,
OmitEmpty: s.OmitEmpty,
OmitFalse: s.OmitFalse,
OmitZero: s.OmitZero,
Name: s.Name,
FieldName: s.FieldName,
ForceString: s.ForceString,
kebabFieldName: s.kebabFieldName,
}
tags := make(map[string]string, len(s.Tags))
for k, v := range s.Tags {
tags[k] = v tags[k] = v
} }
scopy.Tags = tags s.tags = tags
aliases := make([]string, len(s.Aliases)) }
copy(aliases, s.Aliases)
scopy.Aliases = aliases // copyAliases takes a private copy of the aliases, so that they can be
return &scopy // written to without touching the ones this info was read out of.
func (s *SStructFieldInfo) copyAliases() {
aliases := make([]string, len(s.aliases))
copy(aliases, s.aliases)
s.aliases = aliases
} }
func ParseStructFieldJsonInfo(sf reflect.StructField) SStructFieldInfo { func ParseStructFieldJsonInfo(sf reflect.StructField) SStructFieldInfo {
@@ -120,8 +115,8 @@ func ParseFieldJsonInfo(name string, tag reflect.StructTag) SStructFieldInfo {
info.OmitZero = false info.OmitZero = false
info.OmitFalse = false info.OmitFalse = false
info.Tags = utils.TagMap(tag) info.tags = utils.TagMap(tag)
if val, ok := info.Tags["json"]; ok { if val, ok := info.tags["json"]; ok {
keys := strings.Split(val, ",") keys := strings.Split(val, ",")
if len(keys) > 0 { if len(keys) > 0 {
if keys[0] == "-" { if keys[0] == "-" {
@@ -155,14 +150,14 @@ func ParseFieldJsonInfo(name string, tag reflect.StructTag) SStructFieldInfo {
} }
} }
} }
if val, ok := info.Tags["name"]; ok { if val, ok := info.tags["name"]; ok {
info.Name = val info.Name = val
} }
if !info.Ignore && len(info.Name) == 0 { if !info.Ignore && len(info.Name) == 0 {
info.Name = info.kebabFieldName info.Name = info.kebabFieldName
} }
if val, ok := info.Tags["alias"]; !info.Ignore && ok { if val, ok := info.tags["alias"]; !info.Ignore && ok {
info.Aliases = strings.Split(val, ",") info.aliases = strings.Split(val, ",")
} }
return info return info
} }
@@ -179,15 +174,15 @@ func (info *SStructFieldInfo) MarshalName() string {
// Tag returns the value of the tag named name and whether the field has it. // Tag returns the value of the tag named name and whether the field has it.
// A tag without a value is reported as present with an empty value. // A tag without a value is reported as present with an empty value.
func (info *SStructFieldInfo) Tag(name string) (string, bool) { func (info *SStructFieldInfo) Tag(name string) (string, bool) {
val, ok := info.Tags[name] val, ok := info.tags[name]
return val, ok return val, ok
} }
// TagMap returns a copy of the tags of the field, which the caller owns and // TagMap returns a copy of the tags of the field, which the caller owns and
// is free to modify. // is free to modify.
func (info *SStructFieldInfo) TagMap() map[string]string { func (info *SStructFieldInfo) TagMap() map[string]string {
tags := make(map[string]string, len(info.Tags)) tags := make(map[string]string, len(info.tags))
for k, v := range info.Tags { for k, v := range info.tags {
tags[k] = v tags[k] = v
} }
return tags return tags
@@ -375,10 +370,10 @@ func fetchStructFieldValueSet3(dataValue reflect.Value, allocatePtr bool, tags m
continue continue
} }
} }
fieldInfo := fieldInfos[sf.Name].deepCopy() fieldInfo := fieldInfos[sf.Name]
if !fieldInfo.Ignore || includeIgnore { if !fieldInfo.Ignore || includeIgnore {
structFieldVaule := SStructFieldValue{ structFieldVaule := SStructFieldValue{
Info: fieldInfo, Info: &fieldInfo,
Value: fv, Value: fv,
} }
if parent != nil { if parent != nil {
@@ -390,6 +385,7 @@ func fetchStructFieldValueSet3(dataValue reflect.Value, allocatePtr bool, tags m
if len(tags) > 0 { if len(tags) > 0 {
for i := range fields { for i := range fields {
fieldName := fields[i].Info.MarshalName() fieldName := fields[i].Info.MarshalName()
owned := false
for k, v := range tags { for k, v := range tags {
target := "" target := ""
pos := strings.Index(k, "->") pos := strings.Index(k, "->")
@@ -400,7 +396,12 @@ func fetchStructFieldValueSet3(dataValue reflect.Value, allocatePtr bool, tags m
if len(target) > 0 && target != fieldName { if len(target) > 0 && target != fieldName {
continue continue
} }
fields[i].Info.updateTags(k, v) if !owned {
// the tags may still be shared with other callers
fields[i].Info.copyTags()
owned = true
}
fields[i].Info.tags[k] = v
} }
} }
} }
@@ -445,7 +446,7 @@ func (fields SStructFieldValueSet) GetStructFieldIndexes2(name string, strictMod
ret = append(ret, i) ret = append(ret, i)
} else if info.FieldName == capName { } else if info.FieldName == capName {
ret = append(ret, i) ret = append(ret, i)
} else if len(info.Aliases) > 0 && utils.IsInArray(name, info.Aliases) { } else if len(info.aliases) > 0 && utils.IsInArray(name, info.aliases) {
ret = append(ret, i) ret = append(ret, i)
} }
} }