Convert disk booleans from wwbool to *bool

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-09-02 19:43:02 -06:00
committed by Christian Goll
parent cc946e0b99
commit 59f187bf5e
23 changed files with 216 additions and 101 deletions

View File

@@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fixed a bug in `wwclient` which prevented proper cleanup of ephemeral files.
- Fixed `wwclient --debug` to properly enable debug logging.
- Updated golang BuildRequires in RPM specfile. #1990
- Fixed handling of `wwctl` boolean options for disk related seetings.
## v4.6.3, 2025-08-01
@@ -48,7 +49,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `DELETE /api/overlays/{name}?force=true` can delete overlays that are in use
- `warewulfd` overlay autobuild rebuilds overlays after node discovery. #1468
- Improved netplan support. #1873
- fixed boolean options for the disk related seetings
### Fixed

View File

@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func Test_Profile_Set(t *testing.T) {
@@ -187,7 +186,7 @@ nodeprofiles:
default:
disks:
/dev/vda:
wipe_table: "true"
wipe_table: true
partitions:
var:
number: "1"
@@ -205,7 +204,7 @@ nodeprofiles:
default:
disks:
/dev/vda:
wipe_table: "true"
wipe_table: true
partitions:
var:
number: "1"
@@ -220,7 +219,7 @@ nodeprofiles:
default:
disks:
/dev/vda:
wipe_table: "false"
wipe_table: false
partitions:
var:
number: "1"
@@ -232,7 +231,6 @@ nodes: {}`,
},
}
wwlog.SetLogLevel(wwlog.DEBUG)
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
env := testenv.New(t)

View File

@@ -72,7 +72,7 @@ func NodeSetParameterCheck(set *wwapiv1.ConfSetParameter) (nodeDB node.NodesYaml
return
}
// merge in
err = mergo.Merge(nodePtr, &newConf, mergo.WithOverride)
err = mergo.Merge(nodePtr, &newConf, mergo.WithOverride, mergo.WithTransformers(node.Transformer{}))
if err != nil {
return
}

View File

@@ -72,7 +72,7 @@ func ProfileSetParameterCheck(set *wwapiv1.ConfSetParameter) (nodeDB node.NodesY
return
}
// merge in
err = mergo.Merge(profilePtr, &newProfile, mergo.WithOverride)
err = mergo.Merge(profilePtr, &newProfile, mergo.WithOverride, mergo.WithTransformers(node.Transformer{}))
if err != nil {
return
}

View File

@@ -4,6 +4,7 @@ import (
"net"
"github.com/creasty/defaults"
"github.com/warewulf/warewulf/internal/pkg/util"
)
type IPNet net.IPNet
@@ -46,5 +47,5 @@ func (conf *APIConf) Unmarshal(unmarshal func(interface{}) error) error {
}
func (conf APIConf) Enabled() bool {
return BoolP(conf.EnabledP)
return util.BoolP(conf.EnabledP)
}

View File

@@ -1,6 +1,7 @@
package config
import (
"github.com/warewulf/warewulf/internal/pkg/util"
"path"
)
@@ -34,7 +35,7 @@ type TFTPConf struct {
}
func (conf TFTPConf) Enabled() bool {
return BoolP(conf.EnabledP)
return util.BoolP(conf.EnabledP)
}
// WarewulfConf adds additional Warewulf-specific configuration to
@@ -50,19 +51,19 @@ type WarewulfConf struct {
}
func (conf WarewulfConf) Secure() bool {
return BoolP(conf.SecureP)
return util.BoolP(conf.SecureP)
}
func (conf WarewulfConf) AutobuildOverlays() bool {
return BoolP(conf.AutobuildOverlaysP)
return util.BoolP(conf.AutobuildOverlaysP)
}
func (conf WarewulfConf) EnableHostOverlay() bool {
return BoolP(conf.EnableHostOverlayP)
return util.BoolP(conf.EnableHostOverlayP)
}
func (conf WarewulfConf) GrubBoot() bool {
return BoolP(conf.GrubBootP)
return util.BoolP(conf.GrubBootP)
}
func (paths BuildConfig) NodesConf() string {

View File

@@ -1,5 +1,9 @@
package config
import (
"github.com/warewulf/warewulf/internal/pkg/util"
)
// DHCPConf represents the configuration for the DHCP service that
// Warewulf will configure.
type DHCPConf struct {
@@ -11,5 +15,5 @@ type DHCPConf struct {
}
func (conf DHCPConf) Enabled() bool {
return BoolP(conf.EnabledP)
return util.BoolP(conf.EnabledP)
}

View File

@@ -1,5 +1,9 @@
package config
import (
"github.com/warewulf/warewulf/internal/pkg/util"
)
// A MountEntry represents a bind mount that is applied to an image
// during exec and shell.
type MountEntry struct {
@@ -11,9 +15,9 @@ type MountEntry struct {
}
func (mount MountEntry) ReadOnly() bool {
return BoolP(mount.ReadOnlyP)
return util.BoolP(mount.ReadOnlyP)
}
func (mount MountEntry) Copy() bool {
return BoolP(mount.CopyP)
return util.BoolP(mount.CopyP)
}

View File

@@ -2,6 +2,7 @@ package config
import (
"github.com/creasty/defaults"
"github.com/warewulf/warewulf/internal/pkg/util"
)
// NFSConf represents the NFS configuration that will be used by
@@ -14,7 +15,7 @@ type NFSConf struct {
}
func (conf NFSConf) Enabled() bool {
return BoolP(conf.EnabledP)
return util.BoolP(conf.EnabledP)
}
// An NFSExportConf reprents a single NFS export / mount.

View File

@@ -5,10 +5,6 @@ import (
"net"
)
func BoolP(p *bool) bool {
return p != nil && *p
}
func GetOutboundIP() net.IP {
conn, err := net.Dial("udp", "192.0.2.1:80")
if err != nil {

View File

@@ -3,6 +3,7 @@ package node
import (
"net"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwtype"
)
@@ -93,7 +94,7 @@ Holds the disks of a node
*/
type Disk struct {
id string `yaml:"-" json:"-"`
WipeTable wwtype.WWbool `yaml:"wipe_table,omitempty" json:"wipe_table,omitempty" lopt:"diskwipe" comment:"whether or not the partition tables shall be wiped"`
WipeTableP *bool `yaml:"wipe_table,omitempty" json:"wipe_table,omitempty" lopt:"diskwipe" comment:"whether or not the partition tables shall be wiped"`
Partitions map[string]*Partition `yaml:"partitions,omitempty" json:"partitions,omitempty"`
}
@@ -102,29 +103,52 @@ partition definition, the label must be uniq so its used as the key in the
Partitions map
*/
type Partition struct {
id string `yaml:"-" json:"-"`
Number string `yaml:"number,omitempty" json:"number,omitempty" lopt:"partnumber" comment:"Set the partition number, if not set next free slot is used" type:"uint"`
SizeMiB string `yaml:"size_mib,omitempty" json:"size_mib,omitempty" lopt:"partsize" comment:"Set the size of the partition, if not set maximal possible size is used" type:"uint"`
StartMiB string `yaml:"start_mib,omitempty" json:"start_mib,omitempty" comment:"the start of the partition" type:"uint"`
TypeGuid string `yaml:"type_guid,omitempty" json:"type_guid,omitempty" lopt:"parttype" comment:"Set the partition type GUID"`
Guid string `yaml:"guid,omitempty" json:"guid,omitempty" comment:"the GPT unique partition GUID"`
WipePartitionEntry wwtype.WWbool `yaml:"wipe_partition_entry,omitempty" json:"wipe_partition_entry,omitempty" comment:"if true, Ignition will clobber an existing partition if it does not match the config"`
ShouldExist wwtype.WWbool `yaml:"should_exist,omitempty" json:"should_exist,omitempty" lopt:"partcreate" comment:"Create partition if it does not exist"`
Resize wwtype.WWbool `yaml:"resize,omitempty" json:"resize,omitempty" comment:"whether or not the existing partition should be resize"`
id string `yaml:"-" json:"-"`
Number string `yaml:"number,omitempty" json:"number,omitempty" lopt:"partnumber" comment:"Set the partition number, if not set next free slot is used" type:"uint"`
SizeMiB string `yaml:"size_mib,omitempty" json:"size_mib,omitempty" lopt:"partsize" comment:"Set the size of the partition, if not set maximal possible size is used" type:"uint"`
StartMiB string `yaml:"start_mib,omitempty" json:"start_mib,omitempty" comment:"the start of the partition" type:"uint"`
TypeGuid string `yaml:"type_guid,omitempty" json:"type_guid,omitempty" lopt:"parttype" comment:"Set the partition type GUID"`
Guid string `yaml:"guid,omitempty" json:"guid,omitempty" comment:"the GPT unique partition GUID"`
WipePartitionEntryP *bool `yaml:"wipe_partition_entry,omitempty" json:"wipe_partition_entry,omitempty" comment:"if true, Ignition will clobber an existing partition if it does not match the config"`
ShouldExistP *bool `yaml:"should_exist,omitempty" json:"should_exist,omitempty" lopt:"partcreate" comment:"Create partition if it does not exist"`
ResizeP *bool `yaml:"resize,omitempty" json:"resize,omitempty" comment:"whether or not the existing partition should be resize"`
}
/*
Definition of a filesystem. The device is uniq so its used as key
*/
type FileSystem struct {
id string `yaml:"-" json:"-"`
Format string `yaml:"format,omitempty" json:"format,omitempty" lopt:"fsformat" comment:"format of the file system"`
Path string `yaml:"path,omitempty" json:"path,omitempty" lopt:"fspath" comment:"the mount point of the file system"`
WipeFileSystem wwtype.WWbool `yaml:"wipe_filesystem,omitempty" json:"wipe_filesystem,omitempty" lopt:"fswipe" comment:"wipe file system at boot"`
Label string `yaml:"label,omitempty" json:"label,omitempty" comment:"the label of the filesystem"`
Uuid string `yaml:"uuid,omitempty" json:"uuid,omitempty" comment:"the uuid of the filesystem"`
Options []string `yaml:"options,omitempty" json:"options,omitempty" comment:"any additional options to be passed to the format-specific mkfs utility"`
MountOptions string `yaml:"mount_options,omitempty" json:"mount_options,omitempty" comment:"any special options to be passed to the mount command"`
id string `yaml:"-" json:"-"`
Format string `yaml:"format,omitempty" json:"format,omitempty" lopt:"fsformat" comment:"format of the file system"`
Path string `yaml:"path,omitempty" json:"path,omitempty" lopt:"fspath" comment:"the mount point of the file system"`
WipeFileSystemP *bool `yaml:"wipe_filesystem,omitempty" json:"wipe_filesystem,omitempty" lopt:"fswipe" comment:"wipe file system at boot"`
Label string `yaml:"label,omitempty" json:"label,omitempty" comment:"the label of the filesystem"`
Uuid string `yaml:"uuid,omitempty" json:"uuid,omitempty" comment:"the uuid of the filesystem"`
Options []string `yaml:"options,omitempty" json:"options,omitempty" comment:"any additional options to be passed to the format-specific mkfs utility"`
MountOptions string `yaml:"mount_options,omitempty" json:"mount_options,omitempty" comment:"any special options to be passed to the mount command"`
}
type Resource interface{}
// Disk methods
func (disk *Disk) WipeTable() bool {
return util.BoolP(disk.WipeTableP)
}
// Partition methods
func (partition *Partition) WipePartitionEntry() bool {
return util.BoolP(partition.WipePartitionEntryP)
}
func (partition *Partition) ShouldExist() bool {
return util.BoolP(partition.ShouldExistP)
}
func (partition *Partition) Resize() bool {
return util.BoolP(partition.ResizeP)
}
// FileSystem methods
func (fs *FileSystem) WipeFileSystem() bool {
return util.BoolP(fs.WipeFileSystemP)
}

View File

@@ -113,7 +113,7 @@ nodes:
rootfs:
resize: false`,
node: "n1",
field: "Disks[/dev/vda].Partitions[rootfs].Resize",
field: "Disks[/dev/vda].Partitions[rootfs].ResizeP",
value: "false",
},
}

View File

@@ -3,11 +3,60 @@ package node
import (
"net"
"reflect"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/wwtype"
)
// boolPtrFlag implements pflag.Value for *bool fields
type boolPtrFlag struct {
ptr **bool
}
func (f *boolPtrFlag) String() string {
if *f.ptr == nil {
return ""
}
return strconv.FormatBool(**f.ptr)
}
func (f *boolPtrFlag) Set(value string) error {
// Handle unset values
unsetValues := []string{"unset", "delete", "undef", "--", "nil"}
for _, unset := range unsetValues {
if strings.ToLower(value) == unset {
*f.ptr = nil
return nil
}
}
// Handle yes/no
if strings.ToLower(value) == "yes" {
v := true
*f.ptr = &v
return nil
}
if strings.ToLower(value) == "no" {
v := false
*f.ptr = &v
return nil
}
// Parse boolean
if val, err := strconv.ParseBool(value); err == nil {
*f.ptr = &val
return nil
}
return nil
}
func (f *boolPtrFlag) Type() string {
return "bool"
}
type NodeConfDel struct {
TagsDel []string `lopt:"tagdel" comment:"add tags"`
IpmiTagsDel []string `lopt:"ipmitagdel" comment:"delete ipmi tags"`
@@ -173,6 +222,21 @@ func createFlags(baseCmd *cobra.Command,
net.IP{}, // empty default!
myType.Tag.Get("comment"))
}
} else if myType.Type == reflect.TypeOf((*bool)(nil)) {
// Handle *bool type for nullable booleans
ptr := myVal.Addr().Interface().(**bool)
if myType.Tag.Get("sopt") != "" {
baseCmd.PersistentFlags().VarP(&boolPtrFlag{ptr: ptr},
myType.Tag.Get("lopt"),
myType.Tag.Get("sopt"),
myType.Tag.Get("comment"))
baseCmd.Flag(myType.Tag.Get("lopt")).NoOptDefVal = "true"
} else {
baseCmd.PersistentFlags().Var(&boolPtrFlag{ptr: ptr},
myType.Tag.Get("lopt"),
myType.Tag.Get("comment"))
baseCmd.Flag(myType.Tag.Get("lopt")).NoOptDefVal = "true"
}
} else if myType.Type == reflect.TypeOf(wwbool) {
ptr := myVal.Addr().Interface().(*wwtype.WWbool)
if myType.Tag.Get("sopt") != "" {

View File

@@ -69,7 +69,18 @@ func (config *NodesYaml) getProfilesProfiles(input []string, visited map[string]
type Transformer struct{}
func (t Transformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ == reflect.TypeOf(net.IP{}) {
if typ == reflect.TypeOf((*bool)(nil)) {
return func(dst, src reflect.Value) error {
if !src.IsValid() {
return nil
}
// Always override *bool values when source is set
if !src.IsNil() {
dst.Set(src)
}
return nil
}
} else if typ == reflect.TypeOf(net.IP{}) {
return func(dst, src reflect.Value) error {
if !src.IsValid() || src.IsNil() {
return nil

View File

@@ -187,12 +187,16 @@ func recursiveFlatten(obj interface{}) (hasContent bool) {
case reflect.Ptr:
if valObj.Elem().Field(i).Addr().IsValid() {
ret := recursiveFlatten((valObj.Elem().Field(i).Interface()))
if !ret {
valObj.Elem().Field(i).Set(reflect.Zero(valObj.Elem().Field(i).Type()))
// Handle *bool fields separately as they are not structs
if typeObj.Elem().Field(i).Type == reflect.TypeOf((*bool)(nil)) {
hasContent = !valObj.Elem().Field(i).IsZero() || hasContent
} else {
ret := recursiveFlatten((valObj.Elem().Field(i).Interface()))
if !ret {
valObj.Elem().Field(i).Set(reflect.Zero(valObj.Elem().Field(i).Type()))
}
hasContent = ret || hasContent
}
hasContent = ret || hasContent
}
case reflect.Struct:
ret := recursiveFlatten((valObj.Elem().Field(i).Addr().Interface()))

View File

@@ -111,11 +111,13 @@ func (node *Node) GetIgnitionStorage() (stor types_3_4.Storage, rep string, err
for _, opt := range fs.Options {
fsOption = append(fsOption, types_3_4.FilesystemOption(opt))
}
wipe := fs.WipeFileSystem
myFs := types_3_4.Filesystem{
Device: fsdevice,
Path: &fs.Path,
WipeFilesystem: wipe.BoolPtr(),
Device: fsdevice,
Path: &fs.Path,
}
if fs.WipeFileSystemP != nil {
wipeFs := fs.WipeFileSystem()
myFs.WipeFilesystem = &wipeFs
}
if fs.Format != "" {
myFs.Format = &fs.Format
@@ -142,9 +144,6 @@ func (node *Node) GetIgnitionStorage() (stor types_3_4.Storage, rep string, err
for diskDev, disk := range node.Disks {
var partitions []types_3_4.Partition
for partlabel, part := range disk.Partitions {
resize := part.Resize
shouldExist := part.ShouldExist
wipe := part.WipePartitionEntry
label := partlabel
var number int
if part.Number != "" {
@@ -154,16 +153,23 @@ func (node *Node) GetIgnitionStorage() (stor types_3_4.Storage, rep string, err
}
}
myPart := types_3_4.Partition{
Label: &label,
Number: number,
ShouldExist: shouldExist.BoolPtr(),
WipePartitionEntry: wipe.BoolPtr(),
Label: &label,
Number: number,
}
if part.ShouldExistP != nil {
shouldExist := part.ShouldExist()
myPart.ShouldExist = &shouldExist
}
if part.WipePartitionEntryP != nil {
wipeEntry := part.WipePartitionEntry()
myPart.WipePartitionEntry = &wipeEntry
}
if part.Guid != "" {
myPart.GUID = &part.Guid
}
if part.Resize.Bool() {
myPart.Resize = resize.BoolPtr()
if part.ResizeP != nil {
resize := part.Resize()
myPart.Resize = &resize
}
if part.SizeMiB != "" {
var size int
@@ -198,12 +204,15 @@ func (node *Node) GetIgnitionStorage() (stor types_3_4.Storage, rep string, err
}
return partitions[i].Number < partitions[j].Number
})
wipe := disk.WipeTable
disks = append(disks, types_3_4.Disk{
disk_struct := types_3_4.Disk{
Device: diskDev,
Partitions: partitions,
WipeTable: wipe.BoolPtr(),
})
}
if disk.WipeTableP != nil {
wipe := disk.WipeTable()
disk_struct.WipeTable = &wipe
}
disks = append(disks, disk_struct)
}
stor = types_3_4.Storage{
Disks: disks,

View File

@@ -33,8 +33,7 @@ func Test_createIgnitionJson(t *testing.T) {
"partitions": [
{
"label": "scratch",
"shouldExist": true,
"wipePartitionEntry": false
"shouldExist": true
}
],
"wipeTable": true

View File

@@ -710,9 +710,10 @@ func (legacy *Disk) Upgrade() (upgraded *node.Disk) {
upgraded.Partitions[name] = partition.Upgrade()
}
}
err := upgraded.WipeTable.Set(legacy.WipeTable)
if err != nil {
wwlog.Warn("error when parsing legacy.WipeTable: %w", err)
if legacy.WipeTable != "" {
if val, err := strconv.ParseBool(legacy.WipeTable); err == nil {
upgraded.WipeTableP = &val
}
}
return
}
@@ -732,20 +733,23 @@ func (legacy *Partition) Upgrade() (upgraded *node.Partition) {
upgraded = new(node.Partition)
upgraded.Guid = legacy.Guid
upgraded.Number = legacy.Number
err := upgraded.Resize.Set(legacy.Resize)
if err != nil {
wwlog.Warn("error when parsing legacy.Resize: %w", err)
if legacy.Resize != "" {
if val, err := strconv.ParseBool(legacy.Resize); err == nil {
upgraded.ResizeP = &val
}
}
err = upgraded.ShouldExist.Set(legacy.ShouldExist)
if err != nil {
wwlog.Warn("error when parsing legacy.ShouldExist: %w", err)
if legacy.ShouldExist != "" {
if val, err := strconv.ParseBool(legacy.ShouldExist); err == nil {
upgraded.ShouldExistP = &val
}
}
upgraded.SizeMiB = legacy.SizeMiB
upgraded.StartMiB = legacy.StartMiB
upgraded.TypeGuid = legacy.TypeGuid
err = upgraded.WipePartitionEntry.Set(legacy.WipePartitionEntry)
if err != nil {
wwlog.Warn("error when parsing legacy.WipePartitionEntry: %w", err)
if legacy.WipePartitionEntry != "" {
if val, err := strconv.ParseBool(legacy.WipePartitionEntry); err == nil {
upgraded.WipePartitionEntryP = &val
}
}
return
}
@@ -782,9 +786,10 @@ func (legacy *FileSystem) Upgrade() (upgraded *node.FileSystem) {
upgraded.Options = append(upgraded.Options, legacy.Options...)
upgraded.Path = legacy.Path
upgraded.Uuid = legacy.Uuid
err := upgraded.WipeFileSystem.Set(legacy.WipeFileSystem)
if err != nil {
wwlog.Warn("error when parsing legacy.WipeFileSystem: %w", err)
if legacy.WipeFileSystem != "" {
if val, err := strconv.ParseBool(legacy.WipeFileSystem); err == nil {
upgraded.WipeFileSystemP = &val
}
}
return
}

View File

@@ -589,11 +589,11 @@ nodes:
n1:
disks:
/dev/vda:
wipe_table: "true"
wipe_table: true
partitions:
scratch:
number: "1"
should_exist: "true"
should_exist: true
swap:
number: "2"
size_mib: "1024"

View File

@@ -617,3 +617,8 @@ func EqualYaml(a interface{}, b interface{}) (bool, error) {
return bytes.Equal(aYaml, bYaml), nil
}
// BoolP returns the value of a bool pointer, or false if nil
func BoolP(p *bool) bool {
return p != nil && *p
}

View File

@@ -1,4 +1,4 @@
package util
package util_test
import (
"path/filepath"
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/util"
)
func Test_FindFiles(t *testing.T) {
@@ -36,7 +37,7 @@ func Test_FindFiles(t *testing.T) {
env.CreateFile(filepath.Join("/test", file_))
}
files := FindFiles(env.GetPath("/test"))
files := util.FindFiles(env.GetPath("/test"))
assert.Equal(t, tt.findFiles, files)
})
}
@@ -86,7 +87,7 @@ func Test_FindFilterFiles(t *testing.T) {
env.CreateFile(filepath.Join("/test", file_))
}
files, err := FindFilterFiles(env.GetPath("/test"), tt.include, tt.exclude, true)
files, err := util.FindFilterFiles(env.GetPath("/test"), tt.include, tt.exclude, true)
assert.NoError(t, err)
assert.Equal(t, tt.findFiles, files)
})

View File

@@ -33,14 +33,6 @@ func (val WWbool) Bool() bool {
return bval
}
/*
Return a pointer to a bool
*/
func (val WWbool) BoolPtr() *bool {
ret := val.Bool()
return &ret
}
func (val WWbool) BoolDefaultTrue() bool {
str := strings.ToLower(string(val))
if isUnsetValue(str) {

View File

@@ -242,15 +242,12 @@ nodes:
"partitions": [
{
"label": "scratch",
"shouldExist": true,
"wipePartitionEntry": false
"shouldExist": true
},
{
"label": "swap",
"number": 1,
"shouldExist": false,
"sizeMiB": 1024,
"wipePartitionEntry": false
"sizeMiB": 1024
}
],
"wipeTable": true
@@ -266,8 +263,7 @@ nodes:
{
"device": "/dev/disk/by-partlabel/swap",
"format": "swap",
"path": "swap",
"wipeFilesystem": false
"path": "swap"
}
]
}