Skip to content

Commit 404b13a

Browse files
committed
unmarshalling ta config with mapstructure decoder
1 parent 103c475 commit 404b13a

File tree

3 files changed

+129
-10
lines changed

3 files changed

+129
-10
lines changed

cmd/otel-allocator/config/config.go

+125-10
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,24 @@ import (
2121
"fmt"
2222
"io/fs"
2323
"os"
24-
"regexp"
24+
"reflect"
2525
"time"
2626

2727
"github.com/go-logr/logr"
2828
"github.com/prometheus/common/model"
2929
promconfig "github.com/prometheus/prometheus/config"
3030
_ "github.com/prometheus/prometheus/discovery/install"
3131
"github.com/spf13/pflag"
32+
3233
"gopkg.in/yaml.v2"
3334
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3435
"k8s.io/client-go/rest"
3536
"k8s.io/client-go/tools/clientcmd"
3637
"k8s.io/klog/v2"
3738
ctrl "sigs.k8s.io/controller-runtime"
3839
"sigs.k8s.io/controller-runtime/pkg/log/zap"
40+
41+
"github.com/go-viper/mapstructure/v2"
3942
)
4043

4144
const (
@@ -81,6 +84,101 @@ type HTTPSServerConfig struct {
8184
TLSKeyFilePath string `yaml:"tls_key_file_path,omitempty"`
8285
}
8386

87+
// StringToModelDurationHookFunc returns a DecodeHookFuncType
88+
// that converts string to time.Duration, which can be used
89+
// as model.Duration.
90+
func StringToModelDurationHookFunc() mapstructure.DecodeHookFuncType {
91+
return func(
92+
f reflect.Type,
93+
t reflect.Type,
94+
data interface{},
95+
) (interface{}, error) {
96+
if f.Kind() != reflect.String {
97+
return data, nil
98+
}
99+
100+
if t != reflect.TypeOf(model.Duration(5)) {
101+
return data, nil
102+
}
103+
104+
return time.ParseDuration(data.(string))
105+
}
106+
}
107+
108+
// MapToPromConfig returns a DecodeHookFuncType that provides a mechanism
109+
// for decoding promconfig.Config involving its own unmarshal logic.
110+
func MapToPromConfig() mapstructure.DecodeHookFuncType {
111+
return func(
112+
f reflect.Type,
113+
t reflect.Type,
114+
data interface{},
115+
) (interface{}, error) {
116+
if f.Kind() != reflect.Map {
117+
return data, nil
118+
}
119+
120+
if t != reflect.TypeOf(&promconfig.Config{}) {
121+
return data, nil
122+
}
123+
124+
pConfig := &promconfig.Config{}
125+
126+
mb, err := yaml.Marshal(data.(map[any]any))
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
err = yaml.Unmarshal(mb, pConfig)
132+
if err != nil {
133+
return nil, err
134+
}
135+
return pConfig, nil
136+
}
137+
}
138+
139+
// MapToLabelSelector returns a DecodeHookFuncType that
140+
// provides a mechanism for decoding both matchLabels and matchExpressions from camelcase to lowercase
141+
// because we use yaml unmarshaling that supports lowercase field names if no `yaml` tag is defined
142+
// and metav1.LabelSelector uses `json` tags.
143+
// If both the camelcase and lowercase version is present, then the camelcase version takes precedence.
144+
func MapToLabelSelector() mapstructure.DecodeHookFuncType {
145+
return func(
146+
f reflect.Type,
147+
t reflect.Type,
148+
data interface{},
149+
) (interface{}, error) {
150+
if f.Kind() != reflect.Map {
151+
return data, nil
152+
}
153+
154+
if t != reflect.TypeOf(&metav1.LabelSelector{}) {
155+
return data, nil
156+
}
157+
158+
result := &metav1.LabelSelector{}
159+
fMap := data.(map[any]any)
160+
if matchLabels, ok := fMap["matchLabels"]; ok {
161+
fMap["matchlabels"] = matchLabels
162+
delete(fMap, "matchLabels")
163+
}
164+
if matchExpressions, ok := fMap["matchExpressions"]; ok {
165+
fMap["matchexpressions"] = matchExpressions
166+
delete(fMap, "matchExpressions")
167+
}
168+
169+
b, err := yaml.Marshal(fMap)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
err = yaml.Unmarshal(b, result)
175+
if err != nil {
176+
return nil, err
177+
}
178+
return result, nil
179+
}
180+
}
181+
84182
func LoadFromFile(file string, target *Config) error {
85183
return unmarshal(target, file)
86184
}
@@ -154,23 +252,40 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error {
154252
return nil
155253
}
156254

255+
// unmarshal decodes the contents of the configFile into the cfg argument, using a
256+
// mapstructure decoder with the following notable behaviors.
257+
// Decodes time.Duration from strings (see StringToModelDurationHookFunc).
258+
// Allows custom unmarshaling for promconfig.Config struct that implements yaml.Unmarshaler (see MapToPromConfig).
259+
// Allows custom unmarshaling for metav1.LabelSelector struct using both camelcase and lowercase field names (see MapToLabelSelector).
157260
func unmarshal(cfg *Config, configFile string) error {
158261
yamlFile, err := os.ReadFile(configFile)
159262
if err != nil {
160263
return err
161264
}
162265

163-
// Changing matchLabels and matchExpressions from camel case to lower case
164-
// because we use yaml unmarshaling that supports lower case field names if no `yaml` tag is defined
165-
// and metav1.LabelSelector uses `json` tags.
166-
reLabels := regexp.MustCompile(`([ \t\f\v]*)matchLabels:([ \t\f\v]*\n)`)
167-
yamlFile = reLabels.ReplaceAll(yamlFile, []byte("${1}matchlabels:${2}"))
168-
reExpressions := regexp.MustCompile(`([ \t\f\v]*)matchExpressions:([ \t\f\v]*\n)`)
169-
yamlFile = reExpressions.ReplaceAll(yamlFile, []byte("${1}matchexpressions:${2}"))
170-
171-
if err = yaml.Unmarshal(yamlFile, cfg); err != nil {
266+
m := make(map[string]interface{})
267+
if err := yaml.Unmarshal(yamlFile, &m); err != nil {
172268
return fmt.Errorf("error unmarshaling YAML: %w", err)
173269
}
270+
271+
dc := mapstructure.DecoderConfig{
272+
TagName: "yaml",
273+
Result: cfg,
274+
DecodeHook: mapstructure.ComposeDecodeHookFunc(
275+
StringToModelDurationHookFunc(),
276+
MapToPromConfig(),
277+
MapToLabelSelector(),
278+
),
279+
}
280+
281+
decoder, err := mapstructure.NewDecoder(&dc)
282+
if err != nil {
283+
return err
284+
}
285+
if err := decoder.Decode(m); err != nil {
286+
return err
287+
}
288+
174289
return nil
175290
}
176291

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,5 @@ require (
228228
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
229229
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
230230
)
231+
232+
require github.com/go-viper/mapstructure/v2 v2.2.1

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96
236236
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
237237
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
238238
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
239+
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
240+
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
239241
github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=
240242
github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
241243
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=

0 commit comments

Comments
 (0)