From 58e075e78d2292ffdb5a59c0ae0e921fe3ad3307 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Wed, 21 Aug 2024 15:03:44 +0200 Subject: [PATCH 1/9] [common] Upgrade hierarchical KV store to Go generics + unit tests --- common/gera/map.go | 196 ++++++++++--- common/gera/map_test.go | 589 +++++++++++++++++++++++++++++++++++++++ common/gera/stringmap.go | 28 +- go.mod | 2 +- go.sum | 2 + 5 files changed, 763 insertions(+), 54 deletions(-) create mode 100644 common/gera/map_test.go diff --git a/common/gera/map.go b/common/gera/map.go index c3ecfaf3..1d07903b 100644 --- a/common/gera/map.go +++ b/common/gera/map.go @@ -1,7 +1,7 @@ /* * === This file is part of ALICE O² === * - * Copyright 2020 CERN and copyright holders of ALICE O². + * Copyright 2020-2024 CERN and copyright holders of ALICE O². * Author: Teo Mrnjavac * * This program is free software: you can redistribute it and/or modify @@ -29,44 +29,65 @@ package gera import ( + "sync" + "dario.cat/mergo" ) -type Map interface { - Wrap(m Map) Map +type Map[K comparable, V any] interface { + Wrap(m Map[K, V]) Map[K, V] IsHierarchyRoot() bool - HierarchyContains(m Map) bool - Unwrap() Map + HierarchyContains(m Map[K, V]) bool + Unwrap() Map[K, V] - Has(key string) bool + Has(key K) bool Len() int - Get(key string) (interface{}, bool) - Set(key string, value interface{}) bool + Get(key K) (V, bool) + Set(key K, value V) bool + Del(key K) bool - Flattened() (map[string]interface{}, error) - WrappedAndFlattened(m Map) (map[string]interface{}, error) + Flattened() (map[K]V, error) + FlattenedParent() (map[K]V, error) + WrappedAndFlattened(m Map[K, V]) (map[K]V, error) - Raw() map[string]interface{} + Raw() map[K]V + Copy() Map[K, V] + RawCopy() map[K]V } -func MakeMap() Map { - return &WrapMap{ - theMap: make(map[string]interface{}), +func MakeMap[K comparable, V any]() *WrapMap[K, V] { + return &WrapMap[K, V]{ + theMap: make(map[K]V), parent: nil, } } -func MakeMapWithMap(fromMap map[string]interface{}) Map { - myMap := &WrapMap{ +func MakeMapWithMap[K comparable, V any](fromMap map[K]V) *WrapMap[K, V] { + myMap := &WrapMap[K, V]{ theMap: fromMap, parent: nil, } return myMap } -func MakeMapWithMapCopy(fromMap map[string]interface{}) Map { - newBackingMap := make(map[string]interface{}) +func FlattenStack[K comparable, V any](maps ...Map[K, V]) (flattened map[K]V, err error) { + flattenedMap := MakeMap[K, V]() + for _, oneMap := range maps { + var localFlattened map[K]V + localFlattened, err = oneMap.Flattened() + if err != nil { + return + } + flattenedMap = MakeMapWithMap(localFlattened).Wrap(flattenedMap).(*WrapMap[K, V]) + } + + flattened, err = flattenedMap.Flattened() + return +} + +func MakeMapWithMapCopy[K comparable, V any](fromMap map[K]V) *WrapMap[K, V] { + newBackingMap := make(map[K]V) for k, v := range fromMap { newBackingMap[k] = v } @@ -74,16 +95,35 @@ func MakeMapWithMapCopy(fromMap map[string]interface{}) Map { return MakeMapWithMap(newBackingMap) } -type WrapMap struct { - theMap map[string]interface{} - parent Map +type WrapMap[K comparable, V any] struct { + theMap map[K]V + parent Map[K, V] + + unmarshalYAML func(w Map[K, V], unmarshal func(interface{}) error) error + marshalYAML func(w Map[K, V]) (interface{}, error) + + mu sync.RWMutex +} + +func (w *WrapMap[K, V]) WithUnmarshalYAML(unmarshalYAML func(w Map[K, V], unmarshal func(interface{}) error) error) *WrapMap[K, V] { + w.unmarshalYAML = unmarshalYAML + return w +} + +func (w *WrapMap[K, V]) WithMarshalYAML(marshalYAML func(w Map[K, V]) (interface{}, error)) *WrapMap[K, V] { + w.marshalYAML = marshalYAML + return w } -func (w *WrapMap) UnmarshalYAML(unmarshal func(interface{}) error) error { - m := make(map[string]interface{}) +func (w *WrapMap[K, V]) UnmarshalYAML(unmarshal func(interface{}) error) error { + if w.unmarshalYAML != nil { + return w.unmarshalYAML(w, unmarshal) + } + + m := make(map[K]V) err := unmarshal(&m) if err == nil { - *w = WrapMap{ + *w = WrapMap[K, V]{ theMap: m, parent: nil, } @@ -91,17 +131,28 @@ func (w *WrapMap) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } -func (w *WrapMap) IsHierarchyRoot() bool { +func (w *WrapMap[K, V]) MarshalYAML() (interface{}, error) { + if w.marshalYAML != nil { + return w.marshalYAML(w) + } + + return w.theMap, nil +} + +func (w *WrapMap[K, V]) IsHierarchyRoot() bool { if w == nil || w.parent != nil { return false } return true } -func (w *WrapMap) HierarchyContains(m Map) bool { +func (w *WrapMap[K, V]) HierarchyContains(m Map[K, V]) bool { if w == nil || w.parent == nil { return false } + if w == m { + return true + } if w.parent == m { return true } @@ -110,7 +161,7 @@ func (w *WrapMap) HierarchyContains(m Map) bool { // Wraps this map around the gera.Map m, which becomes the new parent. // Returns a pointer to the composite map (i.e. to itself in its new state). -func (w *WrapMap) Wrap(m Map) Map { +func (w *WrapMap[K, V]) Wrap(m Map[K, V]) Map[K, V] { if w == nil { return nil } @@ -120,7 +171,7 @@ func (w *WrapMap) Wrap(m Map) Map { // Unwraps this map from its parent. // Returns a pointer to the former parent which was just unwrapped. -func (w *WrapMap) Unwrap() Map { +func (w *WrapMap[K, V]) Unwrap() Map[K, V] { if w == nil { return nil } @@ -129,33 +180,55 @@ func (w *WrapMap) Unwrap() Map { return p } -func (w *WrapMap) Get(key string) (value interface{}, ok bool) { +func (w *WrapMap[K, V]) Get(key K) (value V, ok bool) { if w == nil || w.theMap == nil { - return nil, false + return value, false } + + w.mu.RLock() + defer w.mu.RUnlock() + if val, ok := w.theMap[key]; ok { return val, true } if w.parent != nil { return w.parent.Get(key) } - return nil, false + return value, false } -func (w *WrapMap) Set(key string, value interface{}) (ok bool) { +func (w *WrapMap[K, V]) Set(key K, value V) (ok bool) { if w == nil || w.theMap == nil { return false } + + w.mu.Lock() + defer w.mu.Unlock() + w.theMap[key] = value return true } -func (w *WrapMap) Has(key string) bool { +func (w *WrapMap[K, V]) Del(key K) (ok bool) { + if w == nil || w.theMap == nil { + return false + } + + w.mu.Lock() + defer w.mu.Unlock() + + if _, exists := w.theMap[key]; exists { + delete(w.theMap, key) + } + return true +} + +func (w *WrapMap[K, V]) Has(key K) bool { _, ok := w.Get(key) return ok } -func (w *WrapMap) Len() int { +func (w *WrapMap[K, V]) Len() int { if w == nil || w.theMap == nil { return 0 } @@ -166,12 +239,15 @@ func (w *WrapMap) Len() int { return len(flattened) } -func (w *WrapMap) Flattened() (map[string]interface{}, error) { +func (w *WrapMap[K, V]) Flattened() (map[K]V, error) { if w == nil { return nil, nil } - out := make(map[string]interface{}) + w.mu.RLock() + defer w.mu.RUnlock() + + out := make(map[K]V) for k, v := range w.theMap { out[k] = v } @@ -188,15 +264,32 @@ func (w *WrapMap) Flattened() (map[string]interface{}, error) { return out, err } -func (w *WrapMap) WrappedAndFlattened(m Map) (map[string]interface{}, error) { +func (w *WrapMap[K, V]) FlattenedParent() (map[K]V, error) { + if w == nil { + return nil, nil + } + + if w.parent == nil { + return make(map[K]V), nil + } + + return w.parent.Flattened() +} + +func (w *WrapMap[K, V]) WrappedAndFlattened(m Map[K, V]) (map[K]V, error) { if w == nil { return nil, nil } - out := make(map[string]interface{}) + w.mu.RLock() + + out := make(map[K]V) for k, v := range w.theMap { out[k] = v } + + w.mu.RUnlock() + if m == nil { return out, nil } @@ -210,9 +303,34 @@ func (w *WrapMap) WrappedAndFlattened(m Map) (map[string]interface{}, error) { return out, err } -func (w *WrapMap) Raw() map[string]interface{} { +func (w *WrapMap[K, V]) Raw() map[K]V { // allows unmutexed access to map, can be unsafe! if w == nil { return nil } return w.theMap } + +func (w *WrapMap[K, V]) Copy() Map[K, V] { + if w == nil { + return nil + } + + w.mu.RLock() + defer w.mu.RUnlock() + + newMap := &WrapMap[K, V]{ + theMap: make(map[K]V, len(w.theMap)), + parent: w.parent, + } + for k, v := range w.theMap { + newMap.theMap[k] = v + } + return newMap +} + +func (w *WrapMap[K, V]) RawCopy() map[K]V { // always safe + if w == nil { + return nil + } + return w.Copy().Raw() +} diff --git a/common/gera/map_test.go b/common/gera/map_test.go new file mode 100644 index 00000000..9c14e247 --- /dev/null +++ b/common/gera/map_test.go @@ -0,0 +1,589 @@ +/* + * === This file is part of ALICE O² === + * + * Copyright 2020-2024 CERN and copyright holders of ALICE O². + * Author: Teo Mrnjavac + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * In applying this license CERN does not waive the privileges and + * immunities granted to it by virtue of its status as an + * Intergovernmental Organization or submit itself to any jurisdiction. + */ + +package gera + +import ( + "maps" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" +) + +var _ = Describe("hierarchical key-value store", func() { + const ( + testPayloadDefaultsYAML1 = ` +############################### +# General Configuration Panel +############################### +dcs_enabled: !public + value: "false" + type: bool + label: "DCS" + description: "Enable/disable DCS SOR/EOR commands" + widget: checkBox + panel: General_Configuration + index: 0 +dd_enabled: !public + value: "true" + type: bool + label: "Data Distribution (FLP)" + description: "Enable/disable Data Distribution components running on FLPs (StfBuilder and StfSender)" + widget: checkBox + panel: General_Configuration + index: 1 +pdp_workflow_parameters: !public + value: "QC,CALIB,GPU,CTF,EVENT_DISPLAY" + type: string + label: "Workflow parameters" + description: "Comma-separated list of workflow parameters. Valid parameters are: QC,CALIB,GPU,CTF,EVENT_DISPLAY." + widget: editBox + panel: "EPNs_Workflows" + index: 603 +user: flp +extra_env_vars: "" +` + testPayloadVarsYAML1 = ` +auto_stop_enabled: "{{ auto_stop_timeout != 'none' }}" +ddsched_enabled: "{{ epn_enabled == 'true' && dd_enabled == 'true' }}" +odc_enabled: "{{ epn_enabled }}" +odc_topology_fullname: '{{ epn_enabled == "true" ? odc.GenerateEPNTopologyFullname() : "" }}' +` + testPayloadUserVarsYAML1 = ` +ccdb_enabled: "true" +ccdb_host: "" +dd_enabled: "false" +pdp_workflow_parameters: "" +` + testPayloadDefaultsYAML2 = ` +detector: "" +dpl_workflow: "{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}" +dpl_command: "{{ util.PrefixedOverride( 'dpl_command', 'ctp' ) }}" +stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" +it: "{{ ctp_readout_host }}" +user: "epn" +` + testPayloadVarsYAML2 = ` +detector: "{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}" +# dpl_workflow is set to ctp_dpl_workflow +dpl_workflow: "{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}" +dpl_command: "" +stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" +` + ) + + var ( + stringMap *WrapMap[string, string] + err error + ) + + customUnmarshalYAML := func(w Map[string, string], unmarshal func(interface{}) error) error { + nodes := make(map[string]yaml.Node) + err := unmarshal(&nodes) + if err == nil { + m := make(map[string]string) + for k, v := range nodes { + if v.Kind == yaml.ScalarNode { + m[k] = v.Value + } else if v.Kind == yaml.MappingNode && v.Tag == "!public" { + type auxType struct { + Value string + } + var aux auxType + err = v.Decode(&aux) + if err != nil { + continue + } + m[k] = aux.Value + } + } + + wPtr := w.(*WrapMap[string, string]) + *wPtr = WrapMap[string, string]{ + theMap: m, + parent: nil, + } + } else { + wPtr := w.(*WrapMap[string, string]) + *wPtr = WrapMap[string, string]{ + theMap: make(map[string]string), + parent: nil, + } + } + return err + } + + stringMap = MakeMap[string, string]() + + When("unmarshaling a YAML document into an empty Map", func() { + BeforeEach(func() { + stringMap = MakeMap[string, string]() + Expect(stringMap).NotTo(BeNil()) + }) + It("should unmarshal correctly with the default unmarshaler if the YAML document is a flat key-value map", func() { + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML2), stringMap) + Expect(err).NotTo(HaveOccurred()) + Expect(stringMap).NotTo(BeNil()) + value, ok := stringMap.Get("odc_enabled") + Expect(value).To(BeEmpty()) + Expect(ok).NotTo(BeTrue()) + value, ok = stringMap.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + value, ok = stringMap.Get("dpl_workflow") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + }) + It("should fail to unmarshal with the default unmarshaler if the YAML document is a tree with YAML tags", func() { + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML1), stringMap) + Expect(err).To(HaveOccurred()) + }) + It("should unmarshal correctly with a custom unmarshaler if the YAML document is a flat key-value map", func() { + stringMap = stringMap.WithUnmarshalYAML(customUnmarshalYAML) + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML2), stringMap) + Expect(err).NotTo(HaveOccurred()) + Expect(stringMap).NotTo(BeNil()) + value, ok := stringMap.Get("odc_enabled") + Expect(value).To(BeEmpty()) + Expect(ok).NotTo(BeTrue()) + value, ok = stringMap.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + value, ok = stringMap.Get("dpl_workflow") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + }) + It("should unmarshal correctly with a custom unmarshaler if the YAML document is a tree with YAML tags", func() { + stringMap = stringMap.WithUnmarshalYAML(customUnmarshalYAML) + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML1), stringMap) + Expect(err).NotTo(HaveOccurred()) + Expect(stringMap).NotTo(BeNil()) + value, ok := stringMap.Get("odc_enabled") + Expect(value).To(BeEmpty()) + Expect(ok).NotTo(BeTrue()) + value, ok = stringMap.Get("dcs_enabled") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("false")) + value, ok = stringMap.Get("dd_enabled") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("true")) + }) + }) + When("marshaling into a YAML document from a Map", func() { + BeforeEach(func() { + stringMap = MakeMap[string, string]() + Expect(stringMap).NotTo(BeNil()) + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML2), stringMap) + Expect(err).NotTo(HaveOccurred()) + Expect(stringMap).NotTo(BeNil()) + }) + + It("should marshal correctly", func() { + marshaledYAML, err := yaml.Marshal(stringMap) + Expect(err).NotTo(HaveOccurred()) + Expect(marshaledYAML).NotTo(BeEmpty()) + + reUnmarshaledMap := make(map[string]string) + err = yaml.Unmarshal(marshaledYAML, &reUnmarshaledMap) + Expect(err).NotTo(HaveOccurred()) + Expect(reUnmarshaledMap).NotTo(BeEmpty()) + + unmarshaledTestPayloadDefaultsYAML2 := make(map[string]string) + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML2), &unmarshaledTestPayloadDefaultsYAML2) + Expect(err).NotTo(HaveOccurred()) + Expect(unmarshaledTestPayloadDefaultsYAML2).NotTo(BeEmpty()) + Expect(maps.Equal(reUnmarshaledMap, unmarshaledTestPayloadDefaultsYAML2)).To(BeTrue()) + }) + }) + When("accessing the structure's underlying map", func() { + BeforeEach(func() { + stringMap = MakeMap[string, string]() + Expect(stringMap).NotTo(BeNil()) + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML2), stringMap) + Expect(err).NotTo(HaveOccurred()) + Expect(stringMap).NotTo(BeNil()) + }) + + It("should correctly return a reference to the raw map", func() { + rawMap := stringMap.Raw() + Expect(rawMap).NotTo(BeNil()) + Expect(len(rawMap)).To(Equal(6)) + value, ok := rawMap["odc_enabled"] + Expect(ok).NotTo(BeTrue()) + value, ok = rawMap["detector"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + value, ok = rawMap["dpl_workflow"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + value2, ok := stringMap.Get("dpl_workflow") + Expect(ok).To(BeTrue()) + Expect(value2).To(Equal(value)) + rawMap["detector"] = "test" + value, ok = stringMap.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("test")) + }) + + It("should correctly return a copy of the whole structure", func() { + copy := stringMap.Copy() + Expect(copy).NotTo(BeNil()) + Expect(copy == stringMap).NotTo(BeTrue()) + Expect(copy).To(Equal(stringMap)) + Expect(copy.Raw()).To(Equal(stringMap.Raw())) + Expect(copy.Len()).To(Equal(stringMap.Len())) + copy.Set("detector", "test") + Expect(copy).NotTo(Equal(stringMap)) + Expect(copy.Raw()).NotTo(Equal(stringMap.Raw())) + Expect(copy.Len()).To(Equal(stringMap.Len())) + copy.Set("detector2", "test2") + Expect(copy.Len()).To(Equal(stringMap.Len() + 1)) + }) + + It("should correctly return a copy of the underlying map", func() { + rawCopy := stringMap.RawCopy() + Expect(rawCopy).NotTo(BeNil()) + Expect(len(rawCopy)).To(Equal(6)) + rawCopy["detector"] = "test" + value, ok := stringMap.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + }) + }) + When("wrapping and unwrapping maps", func() { + + defaults1 := MakeMap[string, string]() + Expect(defaults1).NotTo(BeNil()) + + defaults1 = defaults1.WithUnmarshalYAML(customUnmarshalYAML) + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML1), defaults1) + Expect(err).NotTo(HaveOccurred()) + Expect(defaults1).NotTo(BeNil()) + + defaults2 := MakeMap[string, string]() + Expect(defaults2).NotTo(BeNil()) + + err = yaml.Unmarshal([]byte(testPayloadDefaultsYAML2), defaults2) + Expect(err).NotTo(HaveOccurred()) + Expect(defaults2).NotTo(BeNil()) + + vars1 := MakeMap[string, string]() + Expect(vars1).NotTo(BeNil()) + + err = yaml.Unmarshal([]byte(testPayloadVarsYAML1), vars1) + Expect(err).NotTo(HaveOccurred()) + Expect(vars1).NotTo(BeNil()) + + vars2 := MakeMap[string, string]() + Expect(vars2).NotTo(BeNil()) + + err = yaml.Unmarshal([]byte(testPayloadVarsYAML2), vars2) + Expect(err).NotTo(HaveOccurred()) + Expect(vars2).NotTo(BeNil()) + + userVars1 := MakeMap[string, string]() + Expect(userVars1).NotTo(BeNil()) + + err = yaml.Unmarshal([]byte(testPayloadUserVarsYAML1), userVars1) + Expect(err).NotTo(HaveOccurred()) + Expect(userVars1).NotTo(BeNil()) + + var wrappedDefaults, wrappedVars, wrappedUserVars, wrappedAll Map[string, string] + + It("should wrap correctly", func() { + wrappedDefaults = defaults2.Wrap(defaults1) + Expect(wrappedDefaults).NotTo(BeNil()) + Expect(wrappedDefaults).To(Equal(defaults2)) + + wrappedVars = vars2.Wrap(vars1) + Expect(wrappedVars).NotTo(BeNil()) + Expect(wrappedVars).To(Equal(vars2)) + + wrappedUserVars = userVars1 + }) + + It("should unwrap correctly", func() { + unwrapped := vars2.Unwrap() + Expect(unwrapped).NotTo(BeNil()) + Expect(unwrapped).To(Equal(vars1)) + + rewrapped := vars2.Wrap(vars1) + Expect(rewrapped).NotTo(BeNil()) + }) + + var flattenedDefaults, flattenedVars, flattenedUserVars, flattenedAll map[string]string + + It("should flatten correctly", func() { + flattenedDefaults, err = wrappedDefaults.Flattened() + Expect(err).NotTo(HaveOccurred()) + + flattenedVars, err = wrappedVars.Flattened() + Expect(err).NotTo(HaveOccurred()) + + flattenedUserVars, err = wrappedUserVars.Flattened() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should wrap correctly from previously flattened maps", func() { + wrappedAll = MakeMapWithMap(flattenedUserVars).Wrap(MakeMapWithMap(flattenedVars).Wrap(MakeMapWithMap(flattenedDefaults))) + Expect(wrappedAll).NotTo(BeNil()) + flattenedAll, err = wrappedAll.Flattened() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should flatten stack correctly", func() { + flattenedStack, err := FlattenStack(wrappedDefaults, wrappedVars, wrappedUserVars) + Expect(err).NotTo(HaveOccurred()) + Expect(flattenedAll).To(Equal(flattenedStack)) + }) + + It("should flatten parent correctly", func() { + flattenedParent, err := wrappedAll.FlattenedParent() + Expect(err).NotTo(HaveOccurred()) + + wrappedParents := MakeMapWithMap(flattenedVars).Wrap(MakeMapWithMap(flattenedDefaults)) + flattenedWrappedParents, err := wrappedParents.Flattened() + Expect(flattenedParent).To(Equal(flattenedWrappedParents)) + }) + + It("should wrap and flatten correctly", func() { + flattenedParent, err := wrappedAll.FlattenedParent() + Expect(err).NotTo(HaveOccurred()) + + wrappedAndFlattenedParents, err := MakeMapWithMap(flattenedVars).WrappedAndFlattened(MakeMapWithMap(flattenedDefaults)) + Expect(err).NotTo(HaveOccurred()) + + Expect(wrappedAndFlattenedParents).To(Equal(flattenedParent)) + }) + + It("should correctly look into its hierarchy", func() { + ok := wrappedDefaults.HierarchyContains(defaults1) + Expect(ok).To(BeTrue()) + + ok = wrappedDefaults.HierarchyContains(defaults2) + Expect(ok).To(BeTrue()) + + ok = wrappedDefaults.HierarchyContains(vars1) + Expect(ok).NotTo(BeTrue()) + + ok = wrappedDefaults.IsHierarchyRoot() + Expect(ok).To(BeFalse()) + + ok = defaults2.IsHierarchyRoot() + Expect(ok).To(BeFalse()) + + ok = defaults1.IsHierarchyRoot() + Expect(ok).To(BeTrue()) + + ok = userVars1.IsHierarchyRoot() + Expect(ok).To(BeTrue()) + }) + + It("should correctly perform KV Has operation", func() { + // Has + Expect(wrappedAll.Has("odc_enabled")).To(BeTrue()) + Expect(wrappedAll.Has("ccdb_host")).To(BeTrue()) + Expect(wrappedAll.Has("detector")).To(BeTrue()) + Expect(wrappedAll.Has("it")).To(BeTrue()) + Expect(wrappedAll.Has("pdp_workflow_parameters")).To(BeTrue()) + Expect(wrappedAll.Has("invalid_key")).To(BeFalse()) + Expect(wrappedAll.Has("")).To(BeFalse()) + }) + + It("should correctly perform KV Len operation", func() { + Expect(defaults1.Len()).To(Equal(5)) + Expect(len(defaults2.Raw())).To(Equal(6)) + Expect(defaults2.Len()).To(Equal(10)) + Expect(wrappedVars.Len()).To(Equal(8)) + Expect(wrappedAll.Len()).To(Equal(16)) + }) + + It("should correctly perform KV Get operations", func() { + // Get + value, ok := wrappedAll.Get("odc_enabled") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ epn_enabled }}")) + + value, ok = wrappedAll.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}")) + + value, ok = wrappedAll.Get("dpl_workflow") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + + value, ok = wrappedAll.Get("ccdb_enabled") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("true")) + + value, ok = wrappedAll.Get("ccdb_host") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + + value, ok = defaults1.Get("it") + Expect(ok).To(BeFalse()) + Expect(value).To(BeEmpty()) + + value, ok = defaults1.Get("pdp_workflow_parameters") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("QC,CALIB,GPU,CTF,EVENT_DISPLAY")) + + value, ok = wrappedAll.Get("pdp_workflow_parameters") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + }) + + It("should correctly perform KV Set operations", func() { + // Set + ok := userVars1.Set("detector", "") + Expect(ok).To(BeTrue()) + + ok = userVars1.Set("dpl_workflow", "someValue") + Expect(ok).To(BeTrue()) + + ok = defaults2.Set("user", "someone") + Expect(ok).To(BeTrue()) + + ok = defaults2.Set("dpl_command", "someCommand") + Expect(ok).To(BeTrue()) + + // We need to reflatten after setting new values + flattenedDefaults, err = wrappedDefaults.Flattened() + Expect(err).NotTo(HaveOccurred()) + + flattenedVars, err = wrappedVars.Flattened() + Expect(err).NotTo(HaveOccurred()) + + flattenedUserVars, err = wrappedUserVars.Flattened() + Expect(err).NotTo(HaveOccurred()) + + wrappedAll = MakeMapWithMap(flattenedUserVars).Wrap(MakeMapWithMap(flattenedVars).Wrap(MakeMapWithMap(flattenedDefaults))) + + value, ok := wrappedAll.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(BeEmpty()) + + value, ok = wrappedAll.Get("dpl_workflow") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("someValue")) + + value, ok = wrappedAll.Get("user") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("someone")) + + value, ok = wrappedAll.Get("dpl_command") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + + flattenedStack, err := FlattenStack(wrappedDefaults, wrappedVars, wrappedUserVars) + Expect(err).NotTo(HaveOccurred()) + + // ouch!!! this is a bug in the original code + value, ok = flattenedStack["detector"] + Expect(ok).To(BeTrue()) + Expect(value).To(BeEmpty()) + + value, ok = flattenedStack["dpl_workflow"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("someValue")) + + value, ok = flattenedStack["user"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("someone")) + + value, ok = flattenedStack["dpl_command"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + }) + + It("should correctly perform KV Del operations", func() { + // Del + ok := userVars1.Del("detector") + Expect(ok).To(BeTrue()) + + ok = userVars1.Del("dpl_workflow") + Expect(ok).To(BeTrue()) + + ok = defaults2.Del("user") + Expect(ok).To(BeTrue()) + + ok = defaults2.Del("dpl_command") + Expect(ok).To(BeTrue()) + + // We need to reflatten after deleting values + flattenedDefaults, err = wrappedDefaults.Flattened() + Expect(err).NotTo(HaveOccurred()) + + flattenedVars, err = wrappedVars.Flattened() + Expect(err).NotTo(HaveOccurred()) + + flattenedUserVars, err = wrappedUserVars.Flattened() + Expect(err).NotTo(HaveOccurred()) + + wrappedAll = MakeMapWithMap(flattenedUserVars).Wrap(MakeMapWithMap(flattenedVars).Wrap(MakeMapWithMap(flattenedDefaults))) + + value, ok := wrappedAll.Get("detector") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}")) + + value, ok = wrappedAll.Get("dpl_workflow") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + + value, ok = wrappedAll.Get("user") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("flp")) + + value, ok = wrappedAll.Get("dpl_command") + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + + flattenedStack, err := FlattenStack(wrappedDefaults, wrappedVars, wrappedUserVars) + Expect(err).NotTo(HaveOccurred()) + + value, ok = flattenedStack["detector"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}")) + + value, ok = flattenedStack["dpl_workflow"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + + value, ok = flattenedStack["user"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("flp")) + + value, ok = flattenedStack["dpl_command"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("")) + }) + }) +}) + +func TestMap(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Hierarchical Map Test Suite") +} diff --git a/common/gera/stringmap.go b/common/gera/stringmap.go index 13dd1163..a770c82c 100644 --- a/common/gera/stringmap.go +++ b/common/gera/stringmap.go @@ -68,20 +68,20 @@ func MakeStringMapWithMap(fromMap map[string]string) *StringWrapMap { return myMap } -func FlattenStack(stringMaps ...StringMap) (flattened map[string]string, err error) { - flattenedSM := MakeStringMap() - for _, stringMap := range stringMaps { - var localFlattened map[string]string - localFlattened, err = stringMap.Flattened() - if err != nil { - return - } - flattenedSM = MakeStringMapWithMap(localFlattened).Wrap(flattenedSM).(*StringWrapMap) - } - - flattened, err = flattenedSM.Flattened() - return -} +//func FlattenStack(stringMaps ...StringMap) (flattened map[string]string, err error) { +// flattenedSM := MakeStringMap() +// for _, stringMap := range stringMaps { +// var localFlattened map[string]string +// localFlattened, err = stringMap.Flattened() +// if err != nil { +// return +// } +// flattenedSM = MakeStringMapWithMap(localFlattened).Wrap(flattenedSM).(*StringWrapMap) +// } +// +// flattened, err = flattenedSM.Flattened() +// return +//} func MakeStringMapWithMapCopy(fromMap map[string]string) *StringWrapMap { newBackingMap := make(map[string]string) diff --git a/go.mod b/go.mod index a35135cd..032b7b9b 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ require ( ) require ( - dario.cat/mergo v1.0.0 + dario.cat/mergo v1.0.1 github.com/expr-lang/expr v1.16.1 github.com/flosch/pongo2/v6 v6.0.0 github.com/hashicorp/go-multierror v1.1.1 diff --git a/go.sum b/go.sum index 684c6447..0b3d3fd0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= From 7b06f1312b8cbf7226072fd29a47586f3154a2c8 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Wed, 21 Aug 2024 15:32:41 +0200 Subject: [PATCH 2/9] [core] Use gera.Map[string, string] instead of gera.StringMap --- configuration/template/fields.go | 8 ++++---- core/environment/environment.go | 24 ++++++++++++------------ core/task/manager.go | 2 +- core/task/task.go | 16 ++++++++-------- core/task/taskclass/class.go | 30 +++++++++++++++--------------- core/workflow/aggregatorrole.go | 9 +++++---- core/workflow/callrole.go | 9 +++++---- core/workflow/includerole.go | 2 +- core/workflow/iteratorrole.go | 6 +++--- core/workflow/load.go | 2 +- core/workflow/parentadapter.go | 8 ++++---- core/workflow/role.go | 6 +++--- core/workflow/rolebase.go | 32 ++++++++++++++++---------------- walnut/converter/converter.go | 4 ++-- 14 files changed, 80 insertions(+), 78 deletions(-) diff --git a/configuration/template/fields.go b/configuration/template/fields.go index 5bda7967..169b8369 100644 --- a/configuration/template/fields.go +++ b/configuration/template/fields.go @@ -145,9 +145,9 @@ const ( type VarStack struct { Locals map[string]string - Defaults *gera.StringWrapMap - Vars *gera.StringWrapMap - UserVars *gera.StringWrapMap + Defaults *gera.WrapMap[string, string] + Vars *gera.WrapMap[string, string] + UserVars *gera.WrapMap[string, string] } func (vs *VarStack) consolidated(stage Stage) (consolidatedStack map[string]string, err error) { @@ -220,7 +220,7 @@ func (vs *VarStack) consolidated(stage Stage) (consolidatedStack map[string]stri } } - consolidated := gera.MakeStringMapWithMap(vs.Locals).Wrap(gera.MakeStringMapWithMap(userVars).Wrap(gera.MakeStringMapWithMap(vars).Wrap(gera.MakeStringMapWithMap(defaults)))) + consolidated := gera.MakeMapWithMap(vs.Locals).Wrap(gera.MakeMapWithMap(userVars).Wrap(gera.MakeMapWithMap(vars).Wrap(gera.MakeMapWithMap(defaults)))) consolidatedStack, err = consolidated.Flattened() if err != nil { return diff --git a/core/environment/environment.go b/core/environment/environment.go index d3290b88..c2076eef 100644 --- a/core/environment/environment.go +++ b/core/environment/environment.go @@ -74,10 +74,10 @@ type Environment struct { hookHandlerF func(hooks task.Tasks) error incomingEvents chan event.DeviceEvent - GlobalDefaults gera.StringMap // From Consul - GlobalVars gera.StringMap // From Consul - UserVars gera.StringMap // From user input - BaseConfigStack map[string]string // Exclusively from Consul, already flattened for performance + GlobalDefaults gera.Map[string, string] // From Consul + GlobalVars gera.Map[string, string] // From Consul + UserVars gera.Map[string, string] // From user input + BaseConfigStack map[string]string // Exclusively from Consul, already flattened for performance stateChangedCh chan *event.TasksStateChangedEvent unsubscribe chan struct{} @@ -109,9 +109,9 @@ func newEnvironment(userVars map[string]string, newId uid.ID) (env *Environment, incomingEvents: make(chan event.DeviceEvent), // Every Environment instantiation performs a ConfSvc query for defaults and vars // these key-values stay frozen throughout the lifetime of the environment - GlobalDefaults: gera.MakeStringMapWithMap(the.ConfSvc().GetDefaults()), - GlobalVars: gera.MakeStringMapWithMap(the.ConfSvc().GetVars()), - UserVars: gera.MakeStringMapWithMap(userVars), + GlobalDefaults: gera.MakeMapWithMap(the.ConfSvc().GetDefaults()), + GlobalVars: gera.MakeMapWithMap(the.ConfSvc().GetVars()), + UserVars: gera.MakeMapWithMap(userVars), stateChangedCh: make(chan *event.TasksStateChangedEvent), callsPendingAwait: make(map[string]callable.CallsMap), @@ -121,9 +121,9 @@ func newEnvironment(userVars map[string]string, newId uid.ID) (env *Environment, env.wfAdapter = workflow.NewParentAdapter( func() uid.ID { return env.Id() }, func() uint32 { return env.GetCurrentRunNumber() }, - func() gera.StringMap { return env.GlobalDefaults }, - func() gera.StringMap { return env.GlobalVars }, - func() gera.StringMap { return env.UserVars }, + func() gera.Map[string, string] { return env.GlobalDefaults }, + func() gera.Map[string, string] { return env.GlobalVars }, + func() gera.Map[string, string] { return env.UserVars }, func(ev event.Event) { env.Mu.Lock() defer env.Mu.Unlock() @@ -134,8 +134,8 @@ func newEnvironment(userVars map[string]string, newId uid.ID) (env *Environment, ) env.GlobalVars.Set("__fmq_cleanup_count", "0") // initialize to 0 the number of START transitions - env.BaseConfigStack, err = gera.MakeStringMapWithMap(env.GlobalVars.Raw()). - WrappedAndFlattened(gera.MakeStringMapWithMap(env.GlobalDefaults.Raw())) // prepare the base config stack + env.BaseConfigStack, err = gera.MakeMapWithMap(env.GlobalVars.Raw()). + WrappedAndFlattened(gera.MakeMapWithMap(env.GlobalDefaults.Raw())) // prepare the base config stack if err != nil { return nil, err } diff --git a/core/task/manager.go b/core/task/manager.go index 4e01ef7f..6bf956e7 100644 --- a/core/task/manager.go +++ b/core/task/manager.go @@ -165,7 +165,7 @@ func (m *Manager) newTaskForMesosOffer( agentId: offer.AgentID.Value, offerId: offer.ID.Value, taskId: newId, - properties: gera.MakeStringMap().Wrap(m.GetTaskClass(descriptor.TaskClassName).Properties), + properties: gera.MakeMap[string, string]().Wrap(m.GetTaskClass(descriptor.TaskClassName).Properties), executorId: executorId.Value, GetTaskClass: nil, localBindMap: nil, diff --git a/core/task/task.go b/core/task/task.go index 146b1b89..89032ca2 100644 --- a/core/task/task.go +++ b/core/task/task.go @@ -70,9 +70,9 @@ type parentRole interface { SetTask(*Task) GetEnvironmentId() uid.ID CollectOutboundChannels() []channel.Outbound - GetDefaults() gera.StringMap - GetVars() gera.StringMap - GetUserVars() gera.StringMap + GetDefaults() gera.Map[string, string] + GetVars() gera.Map[string, string] + GetUserVars() gera.Map[string, string] ConsolidatedVarStack() (varStack map[string]string, err error) CollectInboundChannels() []channel.Inbound SendEvent(event.Event) @@ -125,7 +125,7 @@ type Task struct { state sm.State safeToStop bool - properties gera.StringMap + properties gera.Map[string, string] GetTaskClass func() *taskclass.Class // ↑ to be filled in by NewTaskForMesosOffer in Manager @@ -313,7 +313,7 @@ func (t *Task) BuildTaskCommand(role parentRole) (err error) { return fmt.Errorf("cannot resolve templates for task defaults: %w", err) } - varStack, err = gera.MakeStringMapWithMap(varStack).WrappedAndFlattened(gera.MakeStringMapWithMap(localDefaults)) + varStack, err = gera.MakeMapWithMap(varStack).WrappedAndFlattened(gera.MakeMapWithMap(localDefaults)) if err != nil { log.WithError(err). WithField("partition", role.GetEnvironmentId().String()). @@ -339,7 +339,7 @@ func (t *Task) BuildTaskCommand(role parentRole) (err error) { // We wrap the parent varStack around the task's already processed Defaults, // ensuring that any taskclass Defaults are overridden by anything else. - varStack, err = gera.MakeStringMapWithMap(varStack).WrappedAndFlattened(gera.MakeStringMapWithMap(localVars)) + varStack, err = gera.MakeMapWithMap(varStack).WrappedAndFlattened(gera.MakeMapWithMap(localVars)) if err != nil { log.WithError(err). WithField("partition", role.GetEnvironmentId().String()). @@ -573,12 +573,12 @@ func (t *Task) BuildPropertyMap(bindMap channel.BindMap) (propMap controlcommand // We wrap the parent varStack around the class Defaults+Vars, ensuring // the class Defaults+Vars are overridden by anything else. - classStack := gera.MakeStringMapWithMap(class.Vars.Raw()).Wrap(class.Defaults) + classStack := gera.MakeMapWithMap(class.Vars.Raw()).Wrap(class.Defaults) if err != nil { err = fmt.Errorf("cannot fetch task class defaults for property map: %w", err) return } - varStack, err = gera.MakeStringMapWithMap(varStack).WrappedAndFlattened(classStack) + varStack, err = gera.MakeMapWithMap(varStack).WrappedAndFlattened(classStack) if err != nil { err = fmt.Errorf("cannot fetch task class vars for property map: %w", err) return diff --git a/core/task/taskclass/class.go b/core/task/taskclass/class.go index 234ac06a..902741c6 100644 --- a/core/task/taskclass/class.go +++ b/core/task/taskclass/class.go @@ -59,25 +59,25 @@ func (tcID *Id) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { // the following information is enough to run the task even with no environment or // role Class. type Class struct { - Identifier Id `yaml:"name"` - Defaults gera.StringMap `yaml:"defaults"` - Vars gera.StringMap `yaml:"vars"` + Identifier Id `yaml:"name"` + Defaults gera.Map[string, string] `yaml:"defaults"` + Vars gera.Map[string, string] `yaml:"vars"` Control struct { Mode controlmode.ControlMode `yaml:"mode"` } `yaml:"control"` - Command *common.CommandInfo `yaml:"command"` - Wants ResourceWants `yaml:"wants"` - Limits *ResourceLimits `yaml:"limits"` - Bind []channel.Inbound `yaml:"bind"` - Properties gera.StringMap `yaml:"properties"` - Constraints []constraint.Constraint `yaml:"constraints"` - Connect []channel.Outbound `yaml:"connect"` - UpdatedTimestamp time.Time `yaml:"-"` + Command *common.CommandInfo `yaml:"command"` + Wants ResourceWants `yaml:"wants"` + Limits *ResourceLimits `yaml:"limits"` + Bind []channel.Inbound `yaml:"bind"` + Properties gera.Map[string, string] `yaml:"properties"` + Constraints []constraint.Constraint `yaml:"constraints"` + Connect []channel.Outbound `yaml:"connect"` + UpdatedTimestamp time.Time `yaml:"-"` } func (c *Class) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { // We need to make a fake type to unmarshal into because - // gera.StringMap is an interface + // gera.Map is an interface type _class struct { Identifier Id `yaml:"name"` Defaults map[string]string `yaml:"defaults"` @@ -109,14 +109,14 @@ func (c *Class) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { } *c = Class{ Identifier: aux.Identifier, - Defaults: gera.MakeStringMapWithMap(aux.Defaults), - Vars: gera.MakeStringMapWithMap(aux.Vars), + Defaults: gera.MakeMapWithMap(aux.Defaults), + Vars: gera.MakeMapWithMap(aux.Vars), Control: aux.Control, Command: aux.Command, Wants: aux.Wants, Limits: aux.Limits, Bind: aux.Bind, - Properties: gera.MakeStringMapWithMap(aux.Properties), + Properties: gera.MakeMapWithMap(aux.Properties), Constraints: aux.Constraints, Connect: aux.Connect, UpdatedTimestamp: time.Now(), diff --git a/core/workflow/aggregatorrole.go b/core/workflow/aggregatorrole.go index f3d28720..97c034bc 100644 --- a/core/workflow/aggregatorrole.go +++ b/core/workflow/aggregatorrole.go @@ -26,11 +26,12 @@ package workflow import ( "errors" - "github.com/AliceO2Group/Control/common/gera" "strings" "sync" texttemplate "text/template" + "github.com/AliceO2Group/Control/common/gera" + "github.com/AliceO2Group/Control/common/event" "github.com/AliceO2Group/Control/common/event/topic" pb "github.com/AliceO2Group/Control/common/protos" @@ -54,9 +55,9 @@ func NewAggregatorRole(name string, roles []Role) (r Role) { return &aggregatorRole{ roleBase: roleBase{ Name: name, - Defaults: gera.MakeStringMap(), - Vars: gera.MakeStringMap(), - UserVars: gera.MakeStringMap(), + Defaults: gera.MakeMap[string, string](), + Vars: gera.MakeMap[string, string](), + UserVars: gera.MakeMap[string, string](), }, aggregator: aggregator{Roles: roles}, } diff --git a/core/workflow/callrole.go b/core/workflow/callrole.go index 8d7b67c4..3ee99a88 100644 --- a/core/workflow/callrole.go +++ b/core/workflow/callrole.go @@ -26,11 +26,12 @@ package workflow import ( "errors" - "github.com/AliceO2Group/Control/common/gera" "strings" texttemplate "text/template" "time" + "github.com/AliceO2Group/Control/common/gera" + "github.com/AliceO2Group/Control/common/logger/infologger" "github.com/AliceO2Group/Control/core/task/sm" @@ -56,9 +57,9 @@ func NewCallRole(name string, traits task.Traits, funcCall string, returnVar str return &callRole{ roleBase: roleBase{ Name: name, - Defaults: gera.MakeStringMap(), - Vars: gera.MakeStringMap(), - UserVars: gera.MakeStringMap(), + Defaults: gera.MakeMap[string, string](), + Vars: gera.MakeMap[string, string](), + UserVars: gera.MakeMap[string, string](), }, Traits: traits, FuncCall: funcCall, diff --git a/core/workflow/includerole.go b/core/workflow/includerole.go index 3e9f4717..88371531 100644 --- a/core/workflow/includerole.go +++ b/core/workflow/includerole.go @@ -160,7 +160,7 @@ func (r *includeRole) ProcessTemplates(workflowRepo repos.IRepo, loadSubworkflow } // By now the subworkflow is loaded and reparented to this includeRole. This reparenting is - // needed to ensure the correct gera.StringMap hierarchies, but now that we replace the + // needed to ensure the correct gera.Map hierarchies, but now that we replace the // composed aggregatorRole with the newly loaded one, we must also fix the reparenting and // ensure the loaded name doesn't overwrite the original name of the includeRole. parent := r.parent diff --git a/core/workflow/iteratorrole.go b/core/workflow/iteratorrole.go index 3f3f5115..5a538492 100644 --- a/core/workflow/iteratorrole.go +++ b/core/workflow/iteratorrole.go @@ -363,21 +363,21 @@ func (i *iteratorRole) getConstraints() (cts constraint.Constraints) { return } -func (i *iteratorRole) GetDefaults() gera.StringMap { +func (i *iteratorRole) GetDefaults() gera.Map[string, string] { if i == nil { return nil } return i.template.GetDefaults() } -func (i *iteratorRole) GetVars() gera.StringMap { +func (i *iteratorRole) GetVars() gera.Map[string, string] { if i == nil { return nil } return i.template.GetVars() } -func (i *iteratorRole) GetUserVars() gera.StringMap { +func (i *iteratorRole) GetUserVars() gera.Map[string, string] { if i == nil { return nil } diff --git a/core/workflow/load.go b/core/workflow/load.go index 1a0d227f..f75c0126 100644 --- a/core/workflow/load.go +++ b/core/workflow/load.go @@ -149,7 +149,7 @@ func LoadDPL(tasks []*taskclass.Class, rootRoleName string, extraVarsMap map[str root := new(aggregatorRole) root.roleBase.Name = rootRoleName - root.roleBase.Vars = gera.MakeStringMapWithMap(extraVarsMap) + root.roleBase.Vars = gera.MakeMapWithMap(extraVarsMap) for _, taskItem := range tasks { SingleTaskRole := taskRole{ diff --git a/core/workflow/parentadapter.go b/core/workflow/parentadapter.go index 1ffe5c3b..6fbf58d8 100644 --- a/core/workflow/parentadapter.go +++ b/core/workflow/parentadapter.go @@ -39,7 +39,7 @@ type GetEnvIdFunc func() uid.ID type GetCurrentRunNumberFunc func() uint32 -type GetStringMapFunc func() gera.StringMap +type GetStringMapFunc func() gera.Map[string, string] type SendEvents func(event.Event) @@ -137,15 +137,15 @@ func (*ParentAdapter) CollectOutboundChannels() []channel.Outbound { return make([]channel.Outbound, 0) } -func (p *ParentAdapter) GetDefaults() gera.StringMap { +func (p *ParentAdapter) GetDefaults() gera.Map[string, string] { return p.getDefaultsFunc() } -func (p *ParentAdapter) GetVars() gera.StringMap { +func (p *ParentAdapter) GetVars() gera.Map[string, string] { return p.getVarsFunc() } -func (p *ParentAdapter) GetUserVars() gera.StringMap { +func (p *ParentAdapter) GetUserVars() gera.Map[string, string] { return p.getUserVarsFunc() } diff --git a/core/workflow/role.go b/core/workflow/role.go index 6f85a095..d33f68d2 100644 --- a/core/workflow/role.go +++ b/core/workflow/role.go @@ -92,9 +92,9 @@ type Updatable interface { } type VarNode interface { - GetDefaults() gera.StringMap - GetVars() gera.StringMap - GetUserVars() gera.StringMap + GetDefaults() gera.Map[string, string] + GetVars() gera.Map[string, string] + GetUserVars() gera.Map[string, string] } type copyable interface { diff --git a/core/workflow/rolebase.go b/core/workflow/rolebase.go index 87951cc2..e53c92dc 100644 --- a/core/workflow/rolebase.go +++ b/core/workflow/rolebase.go @@ -56,12 +56,12 @@ type roleBase struct { status SafeStatus state SafeState - Defaults *gera.StringWrapMap `yaml:"defaults,omitempty"` - Vars *gera.StringWrapMap `yaml:"vars,omitempty"` - UserVars *gera.StringWrapMap `yaml:"-"` - Locals map[string]string `yaml:"-"` // only used for passing iterator from template to new role - Bind []channel.Inbound `yaml:"bind,omitempty"` - Enabled string `yaml:"enabled,omitempty"` + Defaults *gera.WrapMap[string, string] `yaml:"defaults,omitempty"` + Vars *gera.WrapMap[string, string] `yaml:"vars,omitempty"` + UserVars *gera.WrapMap[string, string] `yaml:"-"` + Locals map[string]string `yaml:"-"` // only used for passing iterator from template to new role + Bind []channel.Inbound `yaml:"bind,omitempty"` + Enabled string `yaml:"enabled,omitempty"` } func (r *roleBase) IsEnabled() bool { @@ -135,7 +135,7 @@ func (r *roleBase) ConsolidatedVarStack() (varStack map[string]string, err error if err != nil { return } - consolidated := gera.MakeStringMapWithMap(userVars).Wrap(gera.MakeStringMapWithMap(vars).Wrap(gera.MakeStringMapWithMap(defaults))) + consolidated := gera.MakeMapWithMap(userVars).Wrap(gera.MakeMapWithMap(vars).Wrap(gera.MakeMapWithMap(defaults))) varStack, err = consolidated.Flattened() if err != nil { return @@ -238,13 +238,13 @@ func (r *roleBase) UnmarshalYAML(unmarshal func(interface{}) error) (err error) err = unmarshal(&role) if err == nil { if role.Defaults == nil { - role.Defaults = gera.MakeStringMap() + role.Defaults = gera.MakeMap[string, string]() } if role.Vars == nil { - role.Vars = gera.MakeStringMap() + role.Vars = gera.MakeMap[string, string]() } if role.UserVars == nil { - role.UserVars = gera.MakeStringMap() + role.UserVars = gera.MakeMap[string, string]() } *r = roleBase(role) } @@ -317,9 +317,9 @@ func (r *roleBase) copy() copyable { rCopy := roleBase{ Name: r.Name, parent: r.parent, - Defaults: r.Defaults.Copy().(*gera.StringWrapMap), - Vars: r.Vars.Copy().(*gera.StringWrapMap), - UserVars: r.UserVars.Copy().(*gera.StringWrapMap), + Defaults: r.Defaults.Copy().(*gera.WrapMap[string, string]), + Vars: r.Vars.Copy().(*gera.WrapMap[string, string]), + UserVars: r.UserVars.Copy().(*gera.WrapMap[string, string]), Locals: make(map[string]string), Connect: make([]channel.Outbound, len(r.Connect)), Constraints: make(constraint.Constraints, len(r.Constraints)), @@ -465,21 +465,21 @@ func (r *roleBase) getConstraints() (cts constraint.Constraints) { return } -func (r *roleBase) GetDefaults() gera.StringMap { +func (r *roleBase) GetDefaults() gera.Map[string, string] { if r == nil { return nil } return r.Defaults } -func (r *roleBase) GetVars() gera.StringMap { +func (r *roleBase) GetVars() gera.Map[string, string] { if r == nil { return nil } return r.Vars } -func (r *roleBase) GetUserVars() gera.StringMap { +func (r *roleBase) GetUserVars() gera.Map[string, string] { if r == nil { return nil } diff --git a/walnut/converter/converter.go b/walnut/converter/converter.go index 655791ef..0c3bf75a 100644 --- a/walnut/converter/converter.go +++ b/walnut/converter/converter.go @@ -91,7 +91,7 @@ func ExtractTaskClasses(dplDump Dump, taskNamePrefix string, envModules []string Memory: createFloat(128), Ports: port.Ranges{}, // begin - end OR range }, - Properties: gera.MakeStringMapWithMap(map[string]string{ + Properties: gera.MakeMapWithMap(map[string]string{ "severity": "trace", "color": "false", }), @@ -159,7 +159,7 @@ func GenerateTaskTemplate(extractedTasks []*taskclass.Class, outputDir string, d _ = os.MkdirAll(path, os.ModePerm) for _, SingleTask := range extractedTasks { - SingleTask.Defaults = gera.MakeStringMapWithMap(defaults) + SingleTask.Defaults = gera.MakeMapWithMap(defaults) SingleTask.Command.User = createString("{{ user }}") YAMLData, err := yaml.Marshal(&SingleTask) From d2cf281aad1c6966a2239773f8d9b7baec1f8ea0 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Wed, 21 Aug 2024 15:34:43 +0200 Subject: [PATCH 3/9] [core] Remove gera.StringMap --- common/gera/stringmap.go | 326 --------------------------------------- 1 file changed, 326 deletions(-) delete mode 100644 common/gera/stringmap.go diff --git a/common/gera/stringmap.go b/common/gera/stringmap.go deleted file mode 100644 index a770c82c..00000000 --- a/common/gera/stringmap.go +++ /dev/null @@ -1,326 +0,0 @@ -/* - * === This file is part of ALICE O² === - * - * Copyright 2020 CERN and copyright holders of ALICE O². - * Author: Teo Mrnjavac - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * In applying this license CERN does not waive the privileges and - * immunities granted to it by virtue of its status as an - * Intergovernmental Organization or submit itself to any jurisdiction. - */ - -package gera - -import ( - "sync" - - "dario.cat/mergo" - "gopkg.in/yaml.v3" -) - -type StringMap interface { - Wrap(m StringMap) StringMap - IsHierarchyRoot() bool - HierarchyContains(m StringMap) bool - Unwrap() StringMap - - Has(key string) bool - Len() int - - Get(key string) (string, bool) - Set(key string, value string) bool - Del(key string) bool - - Flattened() (map[string]string, error) - FlattenedParent() (map[string]string, error) - WrappedAndFlattened(m StringMap) (map[string]string, error) - - Raw() map[string]string - Copy() StringMap - RawCopy() map[string]string -} - -func MakeStringMap() *StringWrapMap { - return &StringWrapMap{ - theMap: make(map[string]string), - parent: nil, - } -} - -func MakeStringMapWithMap(fromMap map[string]string) *StringWrapMap { - myMap := &StringWrapMap{ - theMap: fromMap, - parent: nil, - } - return myMap -} - -//func FlattenStack(stringMaps ...StringMap) (flattened map[string]string, err error) { -// flattenedSM := MakeStringMap() -// for _, stringMap := range stringMaps { -// var localFlattened map[string]string -// localFlattened, err = stringMap.Flattened() -// if err != nil { -// return -// } -// flattenedSM = MakeStringMapWithMap(localFlattened).Wrap(flattenedSM).(*StringWrapMap) -// } -// -// flattened, err = flattenedSM.Flattened() -// return -//} - -func MakeStringMapWithMapCopy(fromMap map[string]string) *StringWrapMap { - newBackingMap := make(map[string]string) - for k, v := range fromMap { - newBackingMap[k] = v - } - - return MakeStringMapWithMap(newBackingMap) -} - -type StringWrapMap struct { - theMap map[string]string - parent StringMap - mu sync.RWMutex -} - -func (w *StringWrapMap) UnmarshalYAML(unmarshal func(interface{}) error) error { - nodes := make(map[string]yaml.Node) - err := unmarshal(&nodes) - if err == nil { - m := make(map[string]string) - for k, v := range nodes { - if v.Kind == yaml.ScalarNode { - m[k] = v.Value - } else if v.Kind == yaml.MappingNode && v.Tag == "!public" { - type auxType struct { - Value string - } - var aux auxType - err = v.Decode(&aux) - if err != nil { - continue - } - m[k] = aux.Value - } - } - - *w = StringWrapMap{ - theMap: m, - parent: nil, - } - } else { - *w = StringWrapMap{ - theMap: make(map[string]string), - parent: nil, - } - } - return err -} - -func (w *StringWrapMap) IsHierarchyRoot() bool { - if w == nil || w.parent != nil { - return false - } - return true -} - -func (w *StringWrapMap) HierarchyContains(m StringMap) bool { - if w == nil || w.parent == nil { - return false - } - if w.parent == m { - return true - } - return w.parent.HierarchyContains(m) -} - -// Wraps this map around the gera.Map m, which becomes the new parent. -// Returns a pointer to the composite map (i.e. to itself in its new state). -func (w *StringWrapMap) Wrap(m StringMap) StringMap { - if w == nil { - return nil - } - w.parent = m - return w -} - -// Unwraps this map from its parent. -// Returns a pointer to the former parent which was just unwrapped. -func (w *StringWrapMap) Unwrap() StringMap { - if w == nil { - return nil - } - p := w.parent - w.parent = nil - return p -} - -func (w *StringWrapMap) Get(key string) (value string, ok bool) { - if w == nil || w.theMap == nil { - return "", false - } - - w.mu.RLock() - defer w.mu.RUnlock() - - if val, ok := w.theMap[key]; ok { - return val, true - } - if w.parent != nil { - return w.parent.Get(key) - } - return "", false -} - -func (w *StringWrapMap) Set(key string, value string) (ok bool) { - if w == nil || w.theMap == nil { - return false - } - - w.mu.Lock() - defer w.mu.Unlock() - - w.theMap[key] = value - return true -} - -func (w *StringWrapMap) Del(key string) (ok bool) { - if w == nil || w.theMap == nil { - return false - } - - w.mu.Lock() - defer w.mu.Unlock() - - if _, exists := w.theMap[key]; exists { - delete(w.theMap, key) - } - return true -} - -func (w *StringWrapMap) Has(key string) bool { - _, ok := w.Get(key) - return ok -} - -func (w *StringWrapMap) Len() int { - if w == nil || w.theMap == nil { - return 0 - } - flattened, err := w.Flattened() - if err != nil { - return 0 - } - return len(flattened) -} - -func (w *StringWrapMap) Flattened() (map[string]string, error) { - if w == nil { - return nil, nil - } - - w.mu.RLock() - defer w.mu.RUnlock() - - out := make(map[string]string) - for k, v := range w.theMap { - out[k] = v - } - if w.parent == nil { - return out, nil - } - - flattenedParent, err := w.parent.Flattened() - if err != nil { - return out, err - } - - err = mergo.Merge(&out, flattenedParent) - return out, err -} - -func (w *StringWrapMap) FlattenedParent() (map[string]string, error) { - if w == nil { - return nil, nil - } - - if w.parent == nil { - return make(map[string]string), nil - } - - return w.parent.Flattened() -} - -func (w *StringWrapMap) WrappedAndFlattened(m StringMap) (map[string]string, error) { - if w == nil { - return nil, nil - } - - w.mu.RLock() - - out := make(map[string]string) - for k, v := range w.theMap { - out[k] = v - } - - w.mu.RUnlock() - - if m == nil { - return out, nil - } - - flattenedM, err := m.Flattened() - if err != nil { - return out, err - } - - err = mergo.Merge(&out, flattenedM) - return out, err -} - -func (w *StringWrapMap) Raw() map[string]string { // allows unmutexed access to map, can be unsafe! - if w == nil { - return nil - } - return w.theMap -} - -func (w *StringWrapMap) Copy() StringMap { - if w == nil { - return nil - } - - w.mu.RLock() - defer w.mu.RUnlock() - - newMap := &StringWrapMap{ - theMap: make(map[string]string, len(w.theMap)), - parent: w.parent, - } - for k, v := range w.theMap { - newMap.theMap[k] = v - } - return newMap -} - -func (w *StringWrapMap) RawCopy() map[string]string { // always safe - if w == nil { - return nil - } - return w.Copy().Raw() -} From 6b6c60d7a75f0f86a63044039eb8a12bc294fc36 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Wed, 21 Aug 2024 15:37:19 +0200 Subject: [PATCH 4/9] [build] Add gera to test dirs in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 35eb49e2..a5480de2 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ INSTALL_WHAT:=$(patsubst %, install_%, $(WHAT)) GENERATE_DIRS := ./apricot ./coconut/cmd ./common ./common/runtype ./common/system ./core ./core/integration/ccdb ./core/integration/dcs ./core/integration/ddsched ./core/integration/kafka ./core/integration/odc ./executor ./walnut ./core/integration/trg ./core/integration/bookkeeping SRC_DIRS := ./apricot ./cmd/* ./core ./coconut ./executor ./common ./configuration ./occ/peanut ./walnut -TEST_DIRS := ./apricot/local ./configuration/cfgbackend ./configuration/componentcfg ./configuration/template ./core/task/sm ./core/workflow ./core/integration/odc/fairmq ./core/integration ./core/environment +TEST_DIRS := ./apricot/local ./common/gera ./configuration/cfgbackend ./configuration/componentcfg ./configuration/template ./core/task/sm ./core/workflow ./core/integration/odc/fairmq ./core/integration ./core/environment GO_TEST_DIRS := ./core/repos ./core/integration/dcs coverage:COVERAGE_PREFIX := ./coverage_results From c734fe6dbef2221418b4b248267cb96fefdd8d19 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Thu, 22 Aug 2024 12:46:36 +0200 Subject: [PATCH 5/9] [common] Improve hierarchical KV store test cases --- common/gera/map_test.go | 81 ++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/common/gera/map_test.go b/common/gera/map_test.go index 9c14e247..dea808ee 100644 --- a/common/gera/map_test.go +++ b/common/gera/map_test.go @@ -65,18 +65,6 @@ pdp_workflow_parameters: !public index: 603 user: flp extra_env_vars: "" -` - testPayloadVarsYAML1 = ` -auto_stop_enabled: "{{ auto_stop_timeout != 'none' }}" -ddsched_enabled: "{{ epn_enabled == 'true' && dd_enabled == 'true' }}" -odc_enabled: "{{ epn_enabled }}" -odc_topology_fullname: '{{ epn_enabled == "true" ? odc.GenerateEPNTopologyFullname() : "" }}' -` - testPayloadUserVarsYAML1 = ` -ccdb_enabled: "true" -ccdb_host: "" -dd_enabled: "false" -pdp_workflow_parameters: "" ` testPayloadDefaultsYAML2 = ` detector: "" @@ -85,13 +73,25 @@ dpl_command: "{{ util.PrefixedOverride( 'dpl_command', 'ctp' ) }}" stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" it: "{{ ctp_readout_host }}" user: "epn" +` + testPayloadVarsYAML1 = ` +auto_stop_enabled: "{{ auto_stop_timeout != 'none' }}" +ddsched_enabled: "{{ epn_enabled == 'true' && dd_enabled == 'true' }}" +odc_enabled: "{{ epn_enabled }}" +odc_topology_fullname: '{{ epn_enabled == "true" ? odc.GenerateEPNTopologyFullname() : "" }}' ` testPayloadVarsYAML2 = ` detector: "{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}" # dpl_workflow is set to ctp_dpl_workflow -dpl_workflow: "{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}" +dpl_workflow: "12345" dpl_command: "" stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" +` + testPayloadUserVarsYAML1 = ` +ccdb_enabled: "true" +ccdb_host: "" +dd_enabled: "false" +pdp_workflow_parameters: "" ` ) @@ -330,8 +330,8 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" Expect(unwrapped).NotTo(BeNil()) Expect(unwrapped).To(Equal(vars1)) - rewrapped := vars2.Wrap(vars1) - Expect(rewrapped).NotTo(BeNil()) + wrappedVars = vars2.Wrap(vars1) + Expect(wrappedVars).NotTo(BeNil()) }) var flattenedDefaults, flattenedVars, flattenedUserVars, flattenedAll map[string]string @@ -339,9 +339,31 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" It("should flatten correctly", func() { flattenedDefaults, err = wrappedDefaults.Flattened() Expect(err).NotTo(HaveOccurred()) + Expect(flattenedDefaults).To(Equal(map[string]string{ + "detector": "", + "dpl_workflow": "{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}", + "dpl_command": "{{ util.PrefixedOverride( 'dpl_command', 'ctp' ) }}", + "stfs_shm_segment_size": "{{ ctp_stfs_shm_segment_size }}", + "it": "{{ ctp_readout_host }}", + "dcs_enabled": "false", + "dd_enabled": "true", + "pdp_workflow_parameters": "QC,CALIB,GPU,CTF,EVENT_DISPLAY", + "user": "epn", + "extra_env_vars": "", + })) flattenedVars, err = wrappedVars.Flattened() Expect(err).NotTo(HaveOccurred()) + Expect(flattenedVars).To(Equal(map[string]string{ + "detector": "{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}", + "dpl_workflow": "12345", + "dpl_command": "", + "stfs_shm_segment_size": "{{ ctp_stfs_shm_segment_size }}", + "auto_stop_enabled": "{{ auto_stop_timeout != 'none' }}", + "ddsched_enabled": "{{ epn_enabled == 'true' && dd_enabled == 'true' }}", + "odc_enabled": "{{ epn_enabled }}", + "odc_topology_fullname": "{{ epn_enabled == \"true\" ? odc.GenerateEPNTopologyFullname() : \"\" }}", + })) flattenedUserVars, err = wrappedUserVars.Flattened() Expect(err).NotTo(HaveOccurred()) @@ -352,6 +374,25 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" Expect(wrappedAll).NotTo(BeNil()) flattenedAll, err = wrappedAll.Flattened() Expect(err).NotTo(HaveOccurred()) + + Expect(flattenedAll).To(Equal(map[string]string{ + "detector": "{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}", + "dpl_workflow": "12345", + "dpl_command": "", + "stfs_shm_segment_size": "{{ ctp_stfs_shm_segment_size }}", + "it": "{{ ctp_readout_host }}", + "auto_stop_enabled": "{{ auto_stop_timeout != 'none' }}", + "ddsched_enabled": "{{ epn_enabled == 'true' && dd_enabled == 'true' }}", + "odc_enabled": "{{ epn_enabled }}", + "odc_topology_fullname": "{{ epn_enabled == \"true\" ? odc.GenerateEPNTopologyFullname() : \"\" }}", + "dcs_enabled": "false", + "dd_enabled": "false", + "pdp_workflow_parameters": "", + "user": "epn", + "extra_env_vars": "", + "ccdb_enabled": "true", + "ccdb_host": "", + })) }) It("should flatten stack correctly", func() { @@ -403,7 +444,6 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" }) It("should correctly perform KV Has operation", func() { - // Has Expect(wrappedAll.Has("odc_enabled")).To(BeTrue()) Expect(wrappedAll.Has("ccdb_host")).To(BeTrue()) Expect(wrappedAll.Has("detector")).To(BeTrue()) @@ -422,7 +462,6 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" }) It("should correctly perform KV Get operations", func() { - // Get value, ok := wrappedAll.Get("odc_enabled") Expect(ok).To(BeTrue()) Expect(value).To(Equal("{{ epn_enabled }}")) @@ -433,7 +472,7 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" value, ok = wrappedAll.Get("dpl_workflow") Expect(ok).To(BeTrue()) - Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + Expect(value).To(Equal("12345")) value, ok = wrappedAll.Get("ccdb_enabled") Expect(ok).To(BeTrue()) @@ -457,7 +496,6 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" }) It("should correctly perform KV Set operations", func() { - // Set ok := userVars1.Set("detector", "") Expect(ok).To(BeTrue()) @@ -520,7 +558,6 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" }) It("should correctly perform KV Del operations", func() { - // Del ok := userVars1.Del("detector") Expect(ok).To(BeTrue()) @@ -551,7 +588,7 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" value, ok = wrappedAll.Get("dpl_workflow") Expect(ok).To(BeTrue()) - Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + Expect(value).To(Equal("12345")) value, ok = wrappedAll.Get("user") Expect(ok).To(BeTrue()) @@ -570,7 +607,7 @@ stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" value, ok = flattenedStack["dpl_workflow"] Expect(ok).To(BeTrue()) - Expect(value).To(Equal("{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}")) + Expect(value).To(Equal("12345")) value, ok = flattenedStack["user"] Expect(ok).To(BeTrue()) From 4a0cf62258dca1ce426a3e7ce281134b2fe83f60 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Thu, 22 Aug 2024 12:47:17 +0200 Subject: [PATCH 6/9] [core] Use custom unmarshaler in gera.Map[string, string] instances --- core/workflow/aggregatorrole.go | 6 ++++- core/workflow/rolebase.go | 46 ++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/core/workflow/aggregatorrole.go b/core/workflow/aggregatorrole.go index 97c034bc..f1ae9d7c 100644 --- a/core/workflow/aggregatorrole.go +++ b/core/workflow/aggregatorrole.go @@ -66,7 +66,11 @@ func NewAggregatorRole(name string, roles []Role) (r Role) { func (r *aggregatorRole) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { // NOTE: see NOTE in roleBase.UnmarshalYAML - innerRoleBase := roleBase{} + innerRoleBase := roleBase{ + Defaults: gera.MakeMap[string, string]().WithUnmarshalYAML(kvStoreUnmarshalYAMLWithTags), + Vars: gera.MakeMap[string, string]().WithUnmarshalYAML(kvStoreUnmarshalYAMLWithTags), + UserVars: gera.MakeMap[string, string]().WithUnmarshalYAML(kvStoreUnmarshalYAMLWithTags), + } err = unmarshal(&innerRoleBase) if err != nil { return diff --git a/core/workflow/rolebase.go b/core/workflow/rolebase.go index e53c92dc..2b5dcc42 100644 --- a/core/workflow/rolebase.go +++ b/core/workflow/rolebase.go @@ -36,6 +36,7 @@ import ( "github.com/AliceO2Group/Control/core/task/channel" "github.com/AliceO2Group/Control/core/task/sm" "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" "github.com/AliceO2Group/Control/core/task" "github.com/AliceO2Group/Control/core/task/constraint" @@ -64,6 +65,36 @@ type roleBase struct { Enabled string `yaml:"enabled,omitempty"` } +func kvStoreUnmarshalYAMLWithTags(w gera.Map[string, string], unmarshal func(interface{}) error) error { + nodes := make(map[string]yaml.Node) + err := unmarshal(&nodes) + if err == nil { + m := make(map[string]string) + for k, v := range nodes { + if v.Kind == yaml.ScalarNode { + m[k] = v.Value + } else if v.Kind == yaml.MappingNode && v.Tag == "!public" { + type auxType struct { + Value string + } + var aux auxType + err = v.Decode(&aux) + if err != nil { + continue + } + m[k] = aux.Value + } + } + + wPtr := w.(*gera.WrapMap[string, string]) + *wPtr = *gera.MakeMapWithMap(m) + } else { + wPtr := w.(*gera.WrapMap[string, string]) + *wPtr = *gera.MakeMap[string, string]() + } + return err +} + func (r *roleBase) IsEnabled() bool { // Only valid after ProcessTemplates trimmed := strings.ToLower(strings.TrimSpace(r.Enabled)) @@ -227,9 +258,9 @@ func (r *roleBase) UnmarshalYAML(unmarshal func(interface{}) error) (err error) // recurse back to this function forever type _roleBase roleBase role := _roleBase{ - Defaults: nil, - Vars: nil, - UserVars: nil, + Defaults: gera.MakeMap[string, string]().WithUnmarshalYAML(kvStoreUnmarshalYAMLWithTags), + Vars: gera.MakeMap[string, string]().WithUnmarshalYAML(kvStoreUnmarshalYAMLWithTags), + UserVars: gera.MakeMap[string, string]().WithUnmarshalYAML(kvStoreUnmarshalYAMLWithTags), Locals: make(map[string]string), status: SafeStatus{status: task.INACTIVE}, state: SafeState{state: sm.STANDBY}, @@ -237,15 +268,6 @@ func (r *roleBase) UnmarshalYAML(unmarshal func(interface{}) error) (err error) } err = unmarshal(&role) if err == nil { - if role.Defaults == nil { - role.Defaults = gera.MakeMap[string, string]() - } - if role.Vars == nil { - role.Vars = gera.MakeMap[string, string]() - } - if role.UserVars == nil { - role.UserVars = gera.MakeMap[string, string]() - } *r = roleBase(role) } return From eccc6fee94662069f8fc9121723379a0a44536d4 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Thu, 22 Aug 2024 12:48:57 +0200 Subject: [PATCH 7/9] [common] Fix override with empty value issue OCTRL-916 --- common/gera/map.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/common/gera/map.go b/common/gera/map.go index 1d07903b..5ef482da 100644 --- a/common/gera/map.go +++ b/common/gera/map.go @@ -247,21 +247,21 @@ func (w *WrapMap[K, V]) Flattened() (map[K]V, error) { w.mu.RLock() defer w.mu.RUnlock() - out := make(map[K]V) + thisMapCopy := make(map[K]V) for k, v := range w.theMap { - out[k] = v + thisMapCopy[k] = v } if w.parent == nil { - return out, nil + return thisMapCopy, nil } flattenedParent, err := w.parent.Flattened() if err != nil { - return out, err + return thisMapCopy, err } - err = mergo.Merge(&out, flattenedParent) - return out, err + err = mergo.Merge(&flattenedParent, thisMapCopy, mergo.WithOverride) + return flattenedParent, err } func (w *WrapMap[K, V]) FlattenedParent() (map[K]V, error) { @@ -283,24 +283,24 @@ func (w *WrapMap[K, V]) WrappedAndFlattened(m Map[K, V]) (map[K]V, error) { w.mu.RLock() - out := make(map[K]V) + thisMapCopy := make(map[K]V) for k, v := range w.theMap { - out[k] = v + thisMapCopy[k] = v } w.mu.RUnlock() if m == nil { - return out, nil + return thisMapCopy, nil } flattenedM, err := m.Flattened() if err != nil { - return out, err + return thisMapCopy, err } - err = mergo.Merge(&out, flattenedM) - return out, err + err = mergo.Merge(&flattenedM, thisMapCopy, mergo.WithOverride) + return flattenedM, err } func (w *WrapMap[K, V]) Raw() map[K]V { // allows unmutexed access to map, can be unsafe! From e7191db9488a23029c4cf86bfc19ae9add5b86b8 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Fri, 23 Aug 2024 16:21:46 +0200 Subject: [PATCH 8/9] [core] Add comments on gera and defaults/vars/userVars mechanism --- common/gera/map.go | 4 ++++ core/workflow/rolebase.go | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/common/gera/map.go b/common/gera/map.go index 5ef482da..1cb7d366 100644 --- a/common/gera/map.go +++ b/common/gera/map.go @@ -26,6 +26,10 @@ // // A gera.Map uses a map[string]interface{} as backing store, and it can wrap other gera.Map instances. // Values in child maps override any value provided by a gera.Map that's wrapped in the hierarchy. +// +// The name is reminiscent of hiera, as in hierarchical, but it was deemed desirable to avoid future confusion with the +// Hiera KV store used by Puppet, a different product altogether, so instead of the ancient Greek root, "gera" comes +// from the Italian root instead where "hi" becomes a soft "g". package gera import ( diff --git a/core/workflow/rolebase.go b/core/workflow/rolebase.go index 2b5dcc42..4b8308de 100644 --- a/core/workflow/rolebase.go +++ b/core/workflow/rolebase.go @@ -57,6 +57,19 @@ type roleBase struct { status SafeStatus state SafeState + // Defaults, Vars and UserVars are used to store each role's variables. + // Defaults are the lowest priority, Vars are the second highest, and UserVars are the highest. + // + // These use the gera.Map type, which is a wrapper around a map[string]string that provides hierarchical KV store + // semantics. + // The gera.Map logic allows us to ensure that defaults are overridden by vars, and vars by userVars throughout the + // workflow tree, and at the same time that defaults/vars/userVars set in a child role override the relevant values + // set in its parent role. + // The way we do this is by ensuring parent-child (Wrap) relationships between all the Default members in the + // workflow tree, all the Vars members, and all the UserVars members, and then whenever we need to figure out what's + // the consolidated KV map seen from the point of view of a given role, we Flatten each of these three, and then + // Wrap and re-Flatten between the flattened defaults, vars and userVars (see ConsolidatedVarStack). This results in + // a single map, generatable from the POV of any role within the tree. Defaults *gera.WrapMap[string, string] `yaml:"defaults,omitempty"` Vars *gera.WrapMap[string, string] `yaml:"vars,omitempty"` UserVars *gera.WrapMap[string, string] `yaml:"-"` @@ -65,6 +78,8 @@ type roleBase struct { Enabled string `yaml:"enabled,omitempty"` } +// Needed for the yaml package to correctly unmarshal into gera.Map[string, string] those Defaults and Vars entries from +// a workflow template, that have a !public tag to include input widget metadata. func kvStoreUnmarshalYAMLWithTags(w gera.Map[string, string], unmarshal func(interface{}) error) error { nodes := make(map[string]yaml.Node) err := unmarshal(&nodes) From 67a8d37f596e25d4e83463fc7f320689591f5d83 Mon Sep 17 00:00:00 2001 From: Teo Mrnjavac Date: Thu, 29 Aug 2024 13:47:45 +0200 Subject: [PATCH 9/9] =?UTF-8?q?[core]=20Test=20kvStoreUnmarshalYAMLWithTag?= =?UTF-8?q?s=20and=20YAML=E2=86=92workflow=20unmarshal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/workflow/role_test.go | 263 +++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) diff --git a/core/workflow/role_test.go b/core/workflow/role_test.go index 3deaf667..af6f4f72 100644 --- a/core/workflow/role_test.go +++ b/core/workflow/role_test.go @@ -5,6 +5,7 @@ import ( "github.com/AliceO2Group/Control/core/task/sm" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" ) func complexRoleTree() (root Role, leaves map[string]Role) { @@ -254,4 +255,266 @@ var _ = Describe("role", func() { }) }) }) + + Describe("unmarshaling a YAML workflow template into a tree of roles", func() { + Context("when the YAML template is a simple tree with a single task", func() { + const yamlTemplate = ` +name: root_role +description: description of the root role +defaults: + default1: "true" + default2: value of default2 + default3: false +vars: + var1: "{{ default2 != 'none' }}" + var2: "{{ default1 == 'true' && default3 == 'true' }}" + var3: value of var3 +roles: + - name: "first_role" + enabled: "{{ default1 == 'true' }}" + vars: + var3: "{{ default2 }}" + constraints: + - attribute: some_attribute + value: "{{ default2 }}" + roles: + - name: 'first_subrole' + vars: + var4: '{{default1 == "true" ? var3 : "value of var4"}}' + task: + load: name_of_task1 + - name: "second_subrole" + enabled: 'false' + roles: + - name: "first_subsubrole" + connect: + - name: connection_name + type: pull + target: "{{ Up(2).Path }}.first_subrole:connection_name" + rateLogging: "{{ default1 }}" + task: + load: name_of_task2 + - name: "second_role" + call: + func: testplugin.Noop() + trigger: CONFIGURE + timeout: 1s + critical: false +` + role := new(aggregatorRole) + + It("should unmarshal successfully", func() { + err := yaml.Unmarshal([]byte(yamlTemplate), role) + Expect(err).NotTo(HaveOccurred()) + }) + It("should create a tree with a task role and a call role", func() { + Expect(role.GetName()).To(Equal("root_role")) + Expect(role.GetRoles()).To(HaveLen(2)) + + Expect(role.GetRoles()[0].GetName()).To(Equal("first_role")) + Expect(role.GetRoles()[0]).To(BeAssignableToTypeOf(&aggregatorRole{})) + Expect(role.GetRoles()[0]).NotTo(BeAssignableToTypeOf(&callRole{})) + Expect(role.GetRoles()[0].GetRoles()).To(HaveLen(2)) + + Expect(role.GetRoles()[0].GetRoles()[0].GetName()).To(Equal("first_subrole")) + Expect(role.GetRoles()[0].GetRoles()[0]).To(BeAssignableToTypeOf(&taskRole{})) + + Expect(role.GetRoles()[0].GetRoles()[1].GetName()).To(Equal("second_subrole")) + Expect(role.GetRoles()[0].GetRoles()[1]).To(BeAssignableToTypeOf(&aggregatorRole{})) + Expect(role.GetRoles()[0].GetRoles()[1].GetRoles()).To(HaveLen(1)) + + Expect(role.GetRoles()[0].GetRoles()[1].GetRoles()[0].GetName()).To(Equal("first_subsubrole")) + Expect(role.GetRoles()[0].GetRoles()[1].GetRoles()[0]).To(BeAssignableToTypeOf(&taskRole{})) + + Expect(role.GetRoles()[1].GetName()).To(Equal("second_role")) + Expect(role.GetRoles()[1]).To(BeAssignableToTypeOf(&callRole{})) + Expect(role.GetRoles()[1]).NotTo(BeAssignableToTypeOf(&taskRole{})) + }) + It("should set the variables correctly", func() { + Expect(role.GetDefaults().Raw()).To(HaveLen(3)) + Expect(role.GetRoles()[0].GetRoles()[0].GetVars().Raw()).To(HaveLen(1)) + Expect(role.GetRoles()[0].GetRoles()[1].GetVars().Raw()).To(HaveLen(0)) + Expect(role.GetRoles()[0].GetRoles()[0].ConsolidatedVarStack()).To(HaveLen(7)) + Expect(role.GetRoles()[0].GetRoles()[1].ConsolidatedVarStack()).To(HaveLen(6)) + Expect(role.GetRoles()[0].GetRoles()[0].GetVars().Raw()["var4"]).To(Equal("{{default1 == \"true\" ? var3 : \"value of var4\"}}")) + }) + }) + + Context("when the YAML template resembles readout-dataflow", func() { + const yamlTemplate = ` +name: !public readout-dataflow +description: !public "Main workflow template for ALICE data taking" +defaults: + ############################### + # General Configuration Panel + ############################### + dcs_enabled: !public + value: "false" + type: bool + label: "DCS" + description: "Enable/disable DCS SOR/EOR commands" + widget: checkBox + panel: General_Configuration + index: 0 + dd_enabled: !public + value: "true" + type: bool + label: "Data Distribution (FLP)" + description: "Enable/disable Data Distribution components running on FLPs (StfBuilder and StfSender)" + widget: checkBox + panel: General_Configuration + index: 1 + hosts: '["host1", "host2"]' +vars: + auto_stop_enabled: "{{ auto_stop_timeout != 'none' }}" + ddsched_enabled: "{{ epn_enabled == 'true' && dd_enabled == 'true' }}" +roles: + ########################### + # Start of CTP Readout role + ########################### + - name: "readout-ctp" + enabled: "{{ ctp_readout_enabled == 'true' }}" + vars: + detector: "{{ctp_readout_enabled == 'true' ? inventory.DetectorForHost( ctp_readout_host ) : \"\" }}" + readout_cfg_uri_standalone: "consul-ini://{{ consul_endpoint }}/o2/components/{{config.ResolvePath('readout/' + run_type + '/any/readout-standalone-' + ctp_readout_host)}}" + readout_cfg_uri_stfb: "consul-ini://{{ consul_endpoint }}/o2/components/{{config.Resolve('readout', run_type, 'any', 'readout-stfb-' + ctp_readout_host)}}" + dd_discovery_ib_hostname: "{{ ctp_readout_host }}-ib" # MUST be defined for all stfb and stfs + # dpl_workflow is set to ctp_dpl_workflow + dpl_workflow: "{{ util.PrefixedOverride( 'dpl_workflow', 'ctp' ) }}" + dpl_command: "{{ util.PrefixedOverride( 'dpl_command', 'ctp' ) }}" + stfs_shm_segment_size: "{{ ctp_stfs_shm_segment_size }}" + it: "{{ ctp_readout_host }}" + constraints: + - attribute: machine_id + value: "{{ ctp_readout_host }}" + roles: + - name: "readout" + vars: + readout_cfg_uri: '{{dd_enabled == "true" ? readout_cfg_uri_stfb : readout_cfg_uri_standalone}}' + task: + load: readout-ctp + - name: "data-distribution" + enabled: "{{dd_enabled == 'true' && (qcdd_enabled == 'false' && minimal_dpl_enabled == 'false' && dpl_workflow == 'none' && dpl_command == 'none')}}" + roles: + # stfb-standalone not supported on CTP machine + # if ctp_readout_enabled, we also assume stfb_standalone is false + - name: "stfb" + vars: + dd_discovery_stfb_id: stfb-{{ ctp_readout_host }}-{{ uid.New() }} # must be defined for all stfb roles + connect: + - name: readout + type: pull + target: "{{ Up(2).Path }}.readout:readout" + rateLogging: "{{ fmq_rate_logging }}" + task: + load: stfbuilder-senderoutput + - name: host-{{ it }} + for: + range: "{{ hosts }}" + var: it + vars: + detector: "{{ inventory.DetectorForHost( it ) }}" + readout_cfg_uri_standalone: "consul-ini://{{ consul_endpoint }}/o2/components/{{config.ResolvePath('readout/' + run_type + '/any/readout-standalone-' + it)}}" + readout_cfg_uri_stfb: "consul-ini://{{ consul_endpoint }}/o2/components/{{config.Resolve('readout', run_type, 'any', 'readout-stfb-' + it)}}" + dd_discovery_ib_hostname: "{{ it }}-ib" # MUST be defined for all stfb and stfs + # dpl_workflow is set to _dpl_workflow if such an override exists + dpl_workflow: "{{ util.PrefixedOverride( 'dpl_workflow', strings.ToLower( inventory.DetectorForHost( it ) ) ) }}" + dpl_command: "{{ util.PrefixedOverride( 'dpl_command', strings.ToLower( inventory.DetectorForHost( it ) ) ) }}" + constraints: + - attribute: machine_id + value: "{{ it }}" + roles: + - name: "readout" + vars: + readout_cfg_uri: '{{dd_enabled == "true" ? readout_cfg_uri_stfb : readout_cfg_uri_standalone}}' + task: + load: readout + - name: dcs + enabled: "{{dcs_enabled == 'true'}}" + defaults: + ############################### + # DCS Panel + ############################### + dcs_detectors: "{{ detectors }}" + dcs_sor_parameters: !public + value: "{}" + type: string + label: "Global SOR parameters" + description: "additional parameters for the DCS SOR" + widget: editBox + panel: DCS + index: 2 + visibleif: $$dcs_enabled === "true" + dcs_eor_parameters: !public + value: "{}" + type: string + label: "Global EOR parameters" + description: "additional parameters for the DCS EOR" + widget: editBox + panel: DCS + index: 3 + visibleif: $$dcs_enabled === "true" + roles: + - name: pfr + call: + func: dcs.PrepareForRun() + trigger: before_CONFIGURE + await: after_CONFIGURE + timeout: "{{ dcs_pfr_timeout }}" + critical: false + - name: sor + call: + func: dcs.StartOfRun() + trigger: before_START_ACTIVITY + timeout: "{{ dcs_sor_timeout }}" + critical: true +` + role := new(aggregatorRole) + + It("should unmarshal successfully", func() { + err := yaml.Unmarshal([]byte(yamlTemplate), role) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should create a complex tree correctly", func() { + Expect(role.GetName()).To(Equal("readout-dataflow")) + Expect(role.GetRoles()).To(HaveLen(2)) // GetRoles excludes iterator roles + Expect(role.Roles).To(HaveLen(3)) + Expect(role.GetRoles()[0].GetName()).To(Equal("readout-ctp")) + + Expect(role.Roles[1].GetName()).To(Equal("host-{{ it }}")) + Expect(role.Roles[1]).To(BeAssignableToTypeOf(&iteratorRole{})) + + Expect(role.GetRoles()[1]).To(BeAssignableToTypeOf(&aggregatorRole{})) + Expect(role.GetRoles()[1].GetRoles()).To(HaveLen(2)) + Expect(role.GetRoles()[1].GetRoles()[0]).To(BeAssignableToTypeOf(&callRole{})) + }) + + It("should set the variables correctly", func() { + Expect(role.GetDefaults().Raw()).To(HaveLen(3)) + Expect(role.GetRoles()[0].GetVars().Raw()).To(HaveLen(8)) + Expect(role.Roles[1].GetVars().Raw()).To(HaveLen(6)) + Expect(role.GetRoles()[1].GetVars().Raw()).To(HaveLen(0)) + + // CTP subtree + cvs, err := role.GetRoles()[0].GetRoles()[1].GetRoles()[0].ConsolidatedVarStack() + Expect(err).NotTo(HaveOccurred()) + Expect(cvs).To(HaveLen(14)) + + Expect(cvs["dd_enabled"]).To(Equal("true")) + Expect(cvs["readout_cfg_uri_stfb"]).To(Equal("consul-ini://{{ consul_endpoint }}/o2/components/{{config.Resolve('readout', run_type, 'any', 'readout-stfb-' + ctp_readout_host)}}")) + Expect(cvs["dd_discovery_ib_hostname"]).To(Equal("{{ ctp_readout_host }}-ib")) + Expect(cvs["ddsched_enabled"]).To(Equal("{{ epn_enabled == 'true' && dd_enabled == 'true' }}")) + + // DCS subtree + cvs, err = role.GetRoles()[1].GetRoles()[0].ConsolidatedVarStack() + Expect(err).NotTo(HaveOccurred()) + Expect(cvs).To(HaveLen(8)) + + Expect(cvs["dcs_enabled"]).To(Equal("false")) + Expect(cvs["dcs_detectors"]).To(Equal("{{ detectors }}")) + Expect(cvs["dcs_sor_parameters"]).To(Equal("{}")) + }) + }) + }) })