Skip to content

Commit 6f46574

Browse files
josvazgs-urbaniakGustavo Bazan
authored
CLOUDP-241826: Fix Alert Config translation panic (#2838)
Signed-off-by: jose.vazquez <[email protected]> Co-authored-by: Sergiusz Urbaniak <[email protected]> Co-authored-by: Gustavo Bazan <[email protected]>
1 parent 0ab26fe commit 6f46574

File tree

4 files changed

+194
-47
lines changed

4 files changed

+194
-47
lines changed

internal/cli/alerts/settings/settings.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,9 @@ func (opts *ConfigOpts) newMetricThreshold() *atlasv2.ServerlessMetricThreshold
168168

169169
func (opts *ConfigOpts) newMatcher() map[string]interface{} {
170170
result := make(map[string]interface{})
171-
result["FieldName"] = strings.ToUpper(opts.matcherFieldName)
172-
result["Operator"] = strings.ToUpper(opts.matcherOperator)
173-
result["Value"] = strings.ToUpper(opts.matcherValue)
171+
result["fieldName"] = strings.ToUpper(opts.matcherFieldName)
172+
result["operator"] = strings.ToUpper(opts.matcherOperator)
173+
result["value"] = strings.ToUpper(opts.matcherValue)
174174
return result
175175
}
176176

internal/kubernetes/operator/project/project.go

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
package project
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"strings"
2021

2122
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/features"
2223
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/resources"
2324
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/secrets"
25+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/log"
2426
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer"
2527
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store"
2628
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1"
@@ -683,41 +685,6 @@ func buildAlertConfigurations(acProvider store.AlertConfigurationLister, project
683685
return nil, nil, err
684686
}
685687

686-
convertMatchers := func(atlasMatcher []map[string]interface{}) []akov2.Matcher {
687-
var res []akov2.Matcher
688-
for _, m := range atlasMatcher {
689-
res = append(res, akov2.Matcher{
690-
FieldName: (m["FieldName"]).(string),
691-
Operator: (m["Operator"]).(string),
692-
Value: (m["Value"]).(string),
693-
})
694-
}
695-
return res
696-
}
697-
698-
convertMetricThreshold := func(atlasMT *atlasv2.ServerlessMetricThreshold) *akov2.MetricThreshold {
699-
if atlasMT == nil {
700-
return &akov2.MetricThreshold{}
701-
}
702-
return &akov2.MetricThreshold{
703-
MetricName: atlasMT.MetricName,
704-
Operator: store.StringOrEmpty(atlasMT.Operator),
705-
Threshold: fmt.Sprintf("%f", pointer.GetOrDefault(atlasMT.Threshold, 0.0)),
706-
Units: store.StringOrEmpty(atlasMT.Units),
707-
Mode: store.StringOrEmpty(atlasMT.Mode),
708-
}
709-
}
710-
711-
convertThreshold := func(atlasT *atlasv2.GreaterThanRawThreshold) *akov2.Threshold {
712-
if atlasT == nil {
713-
return &akov2.Threshold{}
714-
}
715-
return &akov2.Threshold{
716-
Operator: store.StringOrEmpty(atlasT.Operator),
717-
Units: store.StringOrEmpty(atlasT.Units),
718-
Threshold: fmt.Sprintf("%d", pointer.GetOrDefault(atlasT.Threshold, 0)),
719-
}
720-
}
721688
convertNotifications := func(atlasNotifications []atlasv2.AlertsNotificationRootForGroup) ([]akov2.Notification, []*corev1.Secret) {
722689
var (
723690
akoNotifications []akov2.Notification
@@ -835,6 +802,81 @@ func buildAlertConfigurations(acProvider store.AlertConfigurationLister, project
835802
return results, secretResults, nil
836803
}
837804

805+
func convertMatchers(atlasMatcher []map[string]interface{}) []akov2.Matcher {
806+
res := make([]akov2.Matcher, 0, len(atlasMatcher))
807+
for _, m := range atlasMatcher {
808+
matcher, err := toMatcher(m)
809+
if err != nil {
810+
_, _ = log.Warningf("Skipping matcher %v, conversion failed: %v\n", m, err.Error())
811+
continue
812+
}
813+
res = append(res, matcher)
814+
}
815+
return res
816+
}
817+
818+
func toMatcher(m map[string]interface{}) (akov2.Matcher, error) {
819+
var matcher akov2.Matcher
820+
if len(m) == 0 {
821+
return matcher, errors.New("empty map cannot be converted to Matcher")
822+
}
823+
fieldName, err := keyAsString(m, "fieldName")
824+
if err != nil {
825+
return matcher, err
826+
}
827+
operator, err := keyAsString(m, "operator")
828+
if err != nil {
829+
return matcher, err
830+
}
831+
value, err := keyAsString(m, "value")
832+
if err != nil {
833+
return matcher, err
834+
}
835+
matcher.FieldName = fieldName
836+
matcher.Operator = operator
837+
matcher.Value = value
838+
return matcher, nil
839+
}
840+
841+
func keyAsString(m map[string]interface{}, key string) (string, error) {
842+
v, ok := m[key]
843+
if !ok {
844+
return "", fmt.Errorf("no key %q in %v", key, m)
845+
}
846+
if v == nil {
847+
return "", fmt.Errorf("%q is unset in %v", key, m)
848+
}
849+
s, ok := (v).(string)
850+
if !ok {
851+
return "", fmt.Errorf("%q is not a string in %v", key, m)
852+
}
853+
return s, nil
854+
}
855+
856+
func convertMetricThreshold(atlasMT *atlasv2.ServerlessMetricThreshold) *akov2.MetricThreshold {
857+
if atlasMT == nil {
858+
return &akov2.MetricThreshold{}
859+
}
860+
return &akov2.MetricThreshold{
861+
MetricName: atlasMT.MetricName,
862+
Operator: store.StringOrEmpty(atlasMT.Operator),
863+
Threshold: fmt.Sprintf("%f", atlasMT.GetThreshold()),
864+
Units: atlasMT.GetUnits(),
865+
Mode: atlasMT.GetMode(),
866+
}
867+
}
868+
869+
func convertThreshold(atlasT *atlasv2.GreaterThanRawThreshold) *akov2.Threshold {
870+
if atlasT == nil {
871+
return &akov2.Threshold{}
872+
}
873+
return &akov2.Threshold{
874+
Operator: store.StringOrEmpty(atlasT.Operator),
875+
Units: store.StringOrEmpty(atlasT.Units),
876+
Threshold: fmt.Sprintf("%d", atlasT.GetThreshold()),
877+
}
878+
}
879+
838880
func generateName(base string) string {
839881
return k8snames.SimpleNameGenerator.GenerateName(base)
840882
}

internal/kubernetes/operator/project/project_test.go

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package project
1818

1919
import (
2020
"fmt"
21+
"log"
2122
"reflect"
2223
"strings"
2324
"testing"
@@ -166,9 +167,9 @@ func TestBuildAtlasProject(t *testing.T) {
166167
EventTypeName: pointer.Get("TestEventTypeName"),
167168
Matchers: &[]map[string]interface{}{
168169
{
169-
"FieldName": "TestFieldName",
170-
"Operator": "TestOperator",
171-
"Value": "TestValue",
170+
"fieldName": "TestFieldName",
171+
"operator": "TestOperator",
172+
"value": "TestValue",
172173
},
173174
},
174175
MetricThreshold: &atlasv2.ServerlessMetricThreshold{
@@ -327,9 +328,9 @@ func TestBuildAtlasProject(t *testing.T) {
327328
}
328329
expectedMatchers := []akov2.Matcher{
329330
{
330-
FieldName: (alertConfigs[0].GetMatchers()[0]["FieldName"]).(string),
331-
Operator: (alertConfigs[0].GetMatchers()[0]["Operator"]).(string),
332-
Value: (alertConfigs[0].GetMatchers()[0]["Value"]).(string),
331+
FieldName: (alertConfigs[0].GetMatchers()[0]["fieldName"]).(string),
332+
Operator: (alertConfigs[0].GetMatchers()[0]["operator"]).(string),
333+
Value: (alertConfigs[0].GetMatchers()[0]["value"]).(string),
333334
},
334335
}
335336
expectedNotifications := []akov2.Notification{
@@ -1326,3 +1327,92 @@ func Test_firstElementOrEmpty(t *testing.T) {
13261327
assert.Equal(t, "1", firstElementOrZeroValue([]string{"1", "2", "3"}))
13271328
})
13281329
}
1330+
1331+
func TestToMatcherErrors(t *testing.T) {
1332+
testCases := []struct {
1333+
title string
1334+
m map[string]interface{}
1335+
expectedErrorMsg string
1336+
}{
1337+
{
1338+
title: "Nil map renders nil map error",
1339+
m: nil,
1340+
expectedErrorMsg: "empty map cannot be converted to Matcher",
1341+
},
1342+
{
1343+
title: "Empty map renders nil map error",
1344+
m: map[string]interface{}{},
1345+
expectedErrorMsg: "empty map cannot be converted to Matcher",
1346+
},
1347+
{
1348+
title: "Missing fieldName renders key not found error",
1349+
m: map[string]interface{}{"blah": 1},
1350+
expectedErrorMsg: "no key \"fieldName\"",
1351+
},
1352+
{
1353+
title: "Misnamed fieldName renders key not found error",
1354+
m: map[string]interface{}{"FieldName": 1},
1355+
expectedErrorMsg: "no key \"fieldName\"",
1356+
},
1357+
{
1358+
title: "Nil fieldName value renders conversion error",
1359+
m: map[string]interface{}{"fieldName": nil},
1360+
expectedErrorMsg: "\"fieldName\" is unset",
1361+
},
1362+
{
1363+
title: "No string fieldName renders conversion error",
1364+
m: map[string]interface{}{"fieldName": 1},
1365+
expectedErrorMsg: "\"fieldName\" is not a string",
1366+
},
1367+
{
1368+
title: "Missing operator renders key not found error",
1369+
m: map[string]interface{}{"fieldName": "blah"},
1370+
expectedErrorMsg: "no key \"operator\"",
1371+
},
1372+
{
1373+
title: "Non string operator renders conversion error",
1374+
m: map[string]interface{}{"fieldName": "blah", "operator": 1},
1375+
expectedErrorMsg: "\"operator\" is not a string",
1376+
},
1377+
{
1378+
title: "Missing value renders key not found error",
1379+
m: map[string]interface{}{"fieldName": "blah", "operator": "op"},
1380+
expectedErrorMsg: "no key \"value\"",
1381+
},
1382+
{
1383+
title: "Non string value renders conversion error",
1384+
m: map[string]interface{}{"fieldName": "blah", "operator": "op", "value": 0},
1385+
expectedErrorMsg: "\"value\" is not a string",
1386+
},
1387+
}
1388+
for _, tc := range testCases {
1389+
t.Run(tc.title, func(t *testing.T) {
1390+
_, err := toMatcher(tc.m)
1391+
log.Printf("err=%v", err)
1392+
assert.ErrorContains(t, err, tc.expectedErrorMsg)
1393+
})
1394+
}
1395+
}
1396+
1397+
func TestConvertMatchers(t *testing.T) {
1398+
maps := []map[string]interface{}{
1399+
nil,
1400+
{},
1401+
{"field": 1},
1402+
{"FieldName": 1},
1403+
{"fieldName": 1},
1404+
{"fieldName": nil},
1405+
{"fieldName": "field"},
1406+
{"fieldName": "field", "operator": 1},
1407+
{"fieldName": "field", "operator": "op"},
1408+
{"fieldName": "field", "operator": "op", "value": 0},
1409+
{"fieldName": "field", "operator": "op", "value": "value"},
1410+
{"fieldName": "field2", "operator": "op2", "value": "other-value"},
1411+
}
1412+
expected := []akov2.Matcher{
1413+
{FieldName: "field", Operator: "op", Value: "value"},
1414+
{FieldName: "field2", Operator: "op2", Value: "other-value"},
1415+
}
1416+
matchers := convertMatchers(maps)
1417+
assert.Equal(t, expected, matchers)
1418+
}

test/e2e/atlas/kubernetes_config_generate_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ func TestProjectWithNonDefaultAlertConf(t *testing.T) {
239239
newAlertConfig := akov2.AlertConfiguration{
240240
Threshold: &akov2.Threshold{},
241241
MetricThreshold: &akov2.MetricThreshold{},
242-
EventTypeName: eventTypeName,
242+
EventTypeName: "HOST_DOWN",
243243
Enabled: true,
244244
Notifications: []akov2.Notification{
245245
{
@@ -270,6 +270,13 @@ func TestProjectWithNonDefaultAlertConf(t *testing.T) {
270270
},
271271
},
272272
},
273+
Matchers: []akov2.Matcher{
274+
{
275+
FieldName: "HOSTNAME",
276+
Operator: "CONTAINS",
277+
Value: "some-name",
278+
},
279+
},
273280
}
274281
expectedProject.Spec.AlertConfigurations = []akov2.AlertConfiguration{
275282
newAlertConfig,
@@ -291,8 +298,11 @@ func TestProjectWithNonDefaultAlertConf(t *testing.T) {
291298
strconv.Itoa(newAlertConfig.Notifications[0].IntervalMin),
292299
"--notificationDelayMin",
293300
strconv.Itoa(*newAlertConfig.Notifications[0].DelayMin),
294-
fmt.Sprintf("--notificationSmsEnabled=%s", strconv.FormatBool(*newAlertConfig.Notifications[0].SMSEnabled)),
295-
fmt.Sprintf("--notificationEmailEnabled=%s", strconv.FormatBool(*newAlertConfig.Notifications[0].EmailEnabled)),
301+
fmt.Sprintf("--notificationSmsEnabled=%s", strconv.FormatBool(pointer.GetOrZero(newAlertConfig.Notifications[0].SMSEnabled))),
302+
fmt.Sprintf("--notificationEmailEnabled=%s", strconv.FormatBool(pointer.GetOrZero(newAlertConfig.Notifications[0].EmailEnabled))),
303+
fmt.Sprintf("--matcherFieldName=%s", newAlertConfig.Matchers[0].FieldName),
304+
fmt.Sprintf("--matcherOperator=%s", newAlertConfig.Matchers[0].Operator),
305+
fmt.Sprintf("--matcherValue=%s", newAlertConfig.Matchers[0].Value),
296306
"-o=json")
297307
cmd.Env = os.Environ()
298308
_, err := cmd.CombinedOutput()
@@ -890,6 +900,11 @@ func checkProject(t *testing.T, output []runtime.Object, expected *akov2.AtlasPr
890900
expected.Spec.AlertConfigurations[i].Notifications[j].ServiceKeyRef = p.Spec.AlertConfigurations[i].Notifications[j].ServiceKeyRef
891901
expected.Spec.AlertConfigurations[i].Notifications[j].VictorOpsSecretRef = p.Spec.AlertConfigurations[i].Notifications[j].VictorOpsSecretRef
892902
}
903+
for k := range alertConfig.Matchers {
904+
expected.Spec.AlertConfigurations[i].Matchers[k].FieldName = p.Spec.AlertConfigurations[i].Matchers[k].FieldName
905+
expected.Spec.AlertConfigurations[i].Matchers[k].Operator = p.Spec.AlertConfigurations[i].Matchers[k].Operator
906+
expected.Spec.AlertConfigurations[i].Matchers[k].Value = p.Spec.AlertConfigurations[i].Matchers[k].Value
907+
}
893908
}
894909

895910
asserts.Equal(expected, p)

0 commit comments

Comments
 (0)