@@ -21,21 +21,24 @@ import (
21
21
"fmt"
22
22
"io/fs"
23
23
"os"
24
- "regexp "
24
+ "reflect "
25
25
"time"
26
26
27
27
"github.com/go-logr/logr"
28
28
"github.com/prometheus/common/model"
29
29
promconfig "github.com/prometheus/prometheus/config"
30
30
_ "github.com/prometheus/prometheus/discovery/install"
31
31
"github.com/spf13/pflag"
32
+
32
33
"gopkg.in/yaml.v2"
33
34
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34
35
"k8s.io/client-go/rest"
35
36
"k8s.io/client-go/tools/clientcmd"
36
37
"k8s.io/klog/v2"
37
38
ctrl "sigs.k8s.io/controller-runtime"
38
39
"sigs.k8s.io/controller-runtime/pkg/log/zap"
40
+
41
+ "github.com/go-viper/mapstructure/v2"
39
42
)
40
43
41
44
const (
@@ -81,6 +84,101 @@ type HTTPSServerConfig struct {
81
84
TLSKeyFilePath string `yaml:"tls_key_file_path,omitempty"`
82
85
}
83
86
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
+
84
182
func LoadFromFile (file string , target * Config ) error {
85
183
return unmarshal (target , file )
86
184
}
@@ -154,23 +252,40 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error {
154
252
return nil
155
253
}
156
254
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).
157
260
func unmarshal (cfg * Config , configFile string ) error {
158
261
yamlFile , err := os .ReadFile (configFile )
159
262
if err != nil {
160
263
return err
161
264
}
162
265
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 {
172
268
return fmt .Errorf ("error unmarshaling YAML: %w" , err )
173
269
}
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
+
174
289
return nil
175
290
}
176
291
0 commit comments