Skip to content

Commit de9e434

Browse files
Merge pull request #2584 from machine424/rrr
OCPBUGS-54516: provide context-rich and case-sensitive config validation
2 parents 29b581b + 45e9052 commit de9e434

File tree

5 files changed

+107
-28
lines changed

5 files changed

+107
-28
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ require (
4242
k8s.io/metrics v0.31.1
4343
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
4444
sigs.k8s.io/controller-runtime v0.19.0
45+
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
4546
sigs.k8s.io/yaml v1.4.0
4647
)
4748

@@ -147,7 +148,6 @@ require (
147148
k8s.io/kms v0.32.1 // indirect
148149
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
149150
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
150-
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
151151
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 // indirect
152152
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
153153
)

pkg/configvalidate/configvalidate_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func TestHandle(t *testing.T) {
8888
},
8989
},
9090
allowed: false,
91-
message: "failed to parse data at key \"config.yaml\": error unmarshaling JSON: while decoding JSON: json: unknown field \"prometheus_operator\"",
91+
message: "failed to parse data at key \"config.yaml\": error unmarshaling: unknown field \"prometheus_operator\"",
9292
},
9393
{
9494
name: "non monitoring configmap",

pkg/manifests/config.go

+46-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ import (
2727
configv1 "github.com/openshift/api/config/v1"
2828
"github.com/prometheus/common/model"
2929
v1 "k8s.io/api/core/v1"
30-
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
30+
jsonutil "k8s.io/apimachinery/pkg/util/json"
3131
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
3232
"k8s.io/klog/v2"
3333
"k8s.io/utils/ptr"
34+
kjson "sigs.k8s.io/json"
35+
kyaml "sigs.k8s.io/yaml"
3436

3537
"github.com/openshift/cluster-monitoring-operator/pkg/metrics"
3638
)
@@ -313,10 +315,51 @@ func (cps CollectionProfiles) String() string {
313315
return sb.String()
314316
}
315317

318+
// Copied from k8s.io/apimachinery/pkg/util/yaml.UnmarshalStrict but using
319+
// sigs.k8s.io/json.UnmarshalStrict instead of encoding/json.UnmarshalStrict
320+
// to enforce case-sensitive unmarshalling and provide more detailed error context.
321+
// This also allows for simpler error messages.
322+
func UnmarshalStrict(data []byte, v interface{}) error {
323+
unmarshalStrict := func(yamlBytes []byte, obj interface{}) error {
324+
jsonBytes, err := kyaml.YAMLToJSONStrict(yamlBytes)
325+
if err != nil {
326+
return err
327+
}
328+
strictErrs, err := kjson.UnmarshalStrict(jsonBytes, obj)
329+
if err != nil {
330+
return fmt.Errorf("error unmarshaling: %w", err)
331+
}
332+
if len(strictErrs) != 0 {
333+
return fmt.Errorf("error unmarshaling: %w", errors.Join(strictErrs...))
334+
}
335+
return nil
336+
}
337+
// Kept for backward compatibility.
338+
switch v := v.(type) {
339+
case *map[string]interface{}:
340+
if err := unmarshalStrict(data, v); err != nil {
341+
return err
342+
}
343+
return jsonutil.ConvertMapNumbers(*v, 0)
344+
case *[]interface{}:
345+
if err := unmarshalStrict(data, v); err != nil {
346+
return err
347+
}
348+
return jsonutil.ConvertSliceNumbers(*v, 0)
349+
case *interface{}:
350+
if err := unmarshalStrict(data, v); err != nil {
351+
return err
352+
}
353+
return jsonutil.ConvertInterfaceNumbers(v, 0)
354+
default:
355+
return unmarshalStrict(data, v)
356+
}
357+
}
358+
316359
func newConfig(content []byte, collectionProfilesFeatureGateEnabled bool) (*Config, error) {
317360
c := Config{CollectionProfilesFeatureGateEnabled: collectionProfilesFeatureGateEnabled}
318361
cmc := defaultClusterMonitoringConfiguration()
319-
err := k8syaml.UnmarshalStrict(content, &cmc)
362+
err := UnmarshalStrict(content, &cmc)
320363
if err != nil {
321364
return nil, err
322365
}
@@ -679,7 +722,7 @@ func NewUserConfigFromString(content string) (*UserWorkloadConfiguration, error)
679722
return NewDefaultUserWorkloadMonitoringConfig(), nil
680723
}
681724
u := &UserWorkloadConfiguration{}
682-
err := k8syaml.UnmarshalStrict([]byte(content), &u)
725+
err := UnmarshalStrict([]byte(content), &u)
683726
if err != nil {
684727
return nil, err
685728
}

pkg/manifests/config_test.go

+58-22
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestNewConfigFromString(t *testing.T) {
2929
tcs := []struct {
3030
name string
3131
configString func() string
32-
shouldFail bool
32+
err string
3333
configCheck func(*Config)
3434
}{
3535
{
@@ -54,22 +54,29 @@ func TestNewConfigFromString(t *testing.T) {
5454
configString: func() string {
5555
return `{"prometheusK8ss": {}}`
5656
},
57-
shouldFail: true,
57+
err: "error unmarshaling: unknown field \"prometheusK8ss\"",
58+
},
59+
{
60+
name: "json string with root field of the wrong case",
61+
configString: func() string {
62+
return `{"PROMETHEUSK8S": {}}`
63+
},
64+
err: "error unmarshaling: unknown field \"PROMETHEUSK8S\"",
5865
},
5966
{
6067
name: "json string with unknown field",
6168
configString: func() string {
6269
return `{"prometheusK8s": {"unknown": "bar"}}`
6370
},
64-
shouldFail: true,
71+
err: "error unmarshaling: unknown field \"prometheusK8s.unknown\"",
6572
},
6673
{
6774
name: "json string with duplicated field",
6875
// users should be aware of this as unmarshalling would only take one part into account.
6976
configString: func() string {
7077
return `{"prometheusK8s": {"foo": {}}, "prometheusK8s": {"bar": {}}}`
7178
},
72-
shouldFail: true,
79+
err: "yaml: unmarshal errors:\n line 1: key \"prometheusK8s\" already set in map",
7380
},
7481
{
7582
name: "empty json string",
@@ -88,7 +95,14 @@ func TestNewConfigFromString(t *testing.T) {
8895
configString: func() string {
8996
return `metricsServe:`
9097
},
91-
shouldFail: true,
98+
err: "error unmarshaling: unknown field \"metricsServe\"",
99+
},
100+
{
101+
name: "yaml string with root field of the wrong case",
102+
configString: func() string {
103+
return `metricserver:`
104+
},
105+
err: "error unmarshaling: unknown field \"metricserver\"",
92106
},
93107
{
94108
name: "yaml string with unknown field",
@@ -97,7 +111,7 @@ func TestNewConfigFromString(t *testing.T) {
97111
metricsServer:
98112
unknown:`
99113
},
100-
shouldFail: true,
114+
err: "error unmarshaling: unknown field \"metricsServer.unknown\"",
101115
},
102116
{
103117
name: "yaml string with duplicated field",
@@ -108,7 +122,13 @@ metricsServer:
108122
metricsServer:
109123
bar:`
110124
},
111-
shouldFail: true,
125+
err: "yaml: unmarshal errors:\n line 5: key \"metricsServer\" already set in map",
126+
},
127+
{
128+
name: "empty yaml string",
129+
configString: func() string {
130+
return ``
131+
},
112132
},
113133
{
114134
name: "empty yaml string",
@@ -121,8 +141,8 @@ metricsServer:
121141
for _, tc := range tcs {
122142
t.Run(tc.name, func(t *testing.T) {
123143
c, err := NewConfigFromString(tc.configString(), false)
124-
if tc.shouldFail {
125-
require.Error(t, err)
144+
if tc.err != "" {
145+
require.ErrorContains(t, err, tc.err)
126146
return
127147
}
128148
require.NoError(t, err)
@@ -137,7 +157,7 @@ func TestNewUserConfigFromString(t *testing.T) {
137157
tcs := []struct {
138158
name string
139159
configString func() string
140-
shouldFail bool
160+
err string
141161
configCheck func(*UserWorkloadConfiguration)
142162
}{
143163
{
@@ -163,22 +183,29 @@ func TestNewUserConfigFromString(t *testing.T) {
163183
configString: func() string {
164184
return `{"unknown": {}}`
165185
},
166-
shouldFail: true,
186+
err: "error unmarshaling: unknown field \"unknown\"",
167187
},
168188
{
169189
name: "json string with unknown field",
170190
configString: func() string {
171191
return `{"prometheusOperator": {"unknown": "bar"}}`
172192
},
173-
shouldFail: true,
193+
err: "error unmarshaling: unknown field \"prometheusOperator.unknown\"",
194+
},
195+
{
196+
name: "json string with field of wrong case",
197+
configString: func() string {
198+
return `{"prometheusOperator": {"nodeselector": ""}}`
199+
},
200+
err: "error unmarshaling: unknown field \"prometheusOperator.nodeselector\"",
174201
},
175202
{
176203
name: "json string with duplicated field",
177204
// users should be aware of this as unmarshalling would only take one part into account.
178205
configString: func() string {
179206
return `{"prometheus": {"foo": {}}, "prometheus": {"bar": {}}}`
180207
},
181-
shouldFail: true,
208+
err: "yaml: unmarshal errors:\n line 1: key \"prometheus\"",
182209
},
183210
{
184211
name: "empty json string",
@@ -197,7 +224,7 @@ func TestNewUserConfigFromString(t *testing.T) {
197224
configString: func() string {
198225
return `unknown:`
199226
},
200-
shouldFail: true,
227+
err: "error unmarshaling: unknown field \"unknown\"",
201228
},
202229
{
203230
name: "yaml string with unknown field",
@@ -206,7 +233,16 @@ func TestNewUserConfigFromString(t *testing.T) {
206233
prometheusOperator:
207234
unknown:`
208235
},
209-
shouldFail: true,
236+
err: "error unmarshaling: unknown field \"prometheusOperator.unknown\"",
237+
},
238+
{
239+
name: "yaml string with field of wrong case",
240+
configString: func() string {
241+
return `
242+
prometheusOperator:
243+
nodeselector:`
244+
},
245+
err: "error unmarshaling: unknown field \"prometheusOperator.nodeselector\"",
210246
},
211247
{
212248
name: "yaml string with duplicated field",
@@ -217,7 +253,7 @@ thanosRuler:
217253
thanosRuler:
218254
bar:`
219255
},
220-
shouldFail: true,
256+
err: "yaml: unmarshal errors:\n line 5: key \"thanosRuler\"",
221257
},
222258
{
223259
name: "empty yaml string",
@@ -230,8 +266,8 @@ thanosRuler:
230266
for _, tc := range tcs {
231267
t.Run(tc.name, func(t *testing.T) {
232268
c, err := NewUserConfigFromString(tc.configString())
233-
if tc.shouldFail {
234-
require.Error(t, err)
269+
if tc.err != "" {
270+
require.ErrorContains(t, err, tc.err)
235271
return
236272
}
237273
require.NoError(t, err)
@@ -729,23 +765,23 @@ func TestCollectionProfilePreCheck(t *testing.T) {
729765
},
730766
{
731767
name: "full_profile",
732-
config: `prometheusk8s:
768+
config: `prometheusK8s:
733769
collectionProfile: full
734770
`,
735771
expected: CollectionProfile("full"),
736772
expectedError: false,
737773
},
738774
{
739775
name: "minimal_profile",
740-
config: `prometheusk8s:
776+
config: `prometheusK8s:
741777
collectionProfile: minimal
742778
`,
743779
expected: CollectionProfile("minimal"),
744780
expectedError: false,
745781
},
746782
{
747783
name: "incorrect_profile",
748-
config: `prometheusk8s:
784+
config: `prometheusK8s:
749785
collectionProfile: foo
750786
`,
751787
expected: "",
@@ -831,7 +867,7 @@ func TestUnsupportedAlertmanagerVersion(t *testing.T) {
831867
name: "using default value",
832868
config: `prometheusK8s:
833869
additionalAlertmanagerConfigs:
834-
- Scheme: foo
870+
- scheme: foo
835871
`,
836872
},
837873
} {

pkg/manifests/manifests_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3747,7 +3747,7 @@ func TestKubeStateMetrics(t *testing.T) {
37473747
}
37483748

37493749
func TestOpenShiftStateMetrics(t *testing.T) {
3750-
config := `openShiftStateMetrics:
3750+
config := `openshiftStateMetrics:
37513751
resources:
37523752
requests:
37533753
cpu: 100m

0 commit comments

Comments
 (0)