Skip to content

Commit ea24c7f

Browse files
authored
Merge pull request #2017 from bruin-data/feat/consolidate-notification-lint-rules
refactor: consolidate notification lint rules into single validator
2 parents 203369c + 67ded14 commit ea24c7f

3 files changed

Lines changed: 195 additions & 514 deletions

File tree

pkg/lint/list.go

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -94,45 +94,17 @@ func GetRules(fs afero.Fs, finder repoFinder, excludeWarnings bool, parser sqlpa
9494
ApplicableLevels: []Level{LevelPipeline},
9595
},
9696
&SimpleRule{
97-
Identifier: "valid-slack-notification",
97+
Identifier: "valid-pipeline-notifications",
9898
Fast: true,
9999
Severity: ValidatorSeverityCritical,
100-
Validator: EnsureSlackFieldInPipelineIsValid,
100+
Validator: EnsurePipelineNotificationsAreValid,
101101
ApplicableLevels: []Level{LevelPipeline},
102102
},
103103
&SimpleRule{
104-
Identifier: "valid-ms-teams-notification",
104+
Identifier: "valid-asset-notifications",
105105
Fast: true,
106106
Severity: ValidatorSeverityCritical,
107-
Validator: EnsureMSTeamsFieldInPipelineIsValid,
108-
ApplicableLevels: []Level{LevelPipeline},
109-
},
110-
&SimpleRule{
111-
Identifier: "valid-discord-notification",
112-
Fast: true,
113-
Severity: ValidatorSeverityCritical,
114-
Validator: EnsureDiscordFieldInPipelineIsValid,
115-
ApplicableLevels: []Level{LevelPipeline},
116-
},
117-
&SimpleRule{
118-
Identifier: "valid-asset-slack-notification",
119-
Fast: true,
120-
Severity: ValidatorSeverityCritical,
121-
AssetValidator: EnsureSlackFieldInAssetIsValid,
122-
ApplicableLevels: []Level{LevelAsset},
123-
},
124-
&SimpleRule{
125-
Identifier: "valid-asset-ms-teams-notification",
126-
Fast: true,
127-
Severity: ValidatorSeverityCritical,
128-
AssetValidator: EnsureMSTeamsFieldInAssetIsValid,
129-
ApplicableLevels: []Level{LevelAsset},
130-
},
131-
&SimpleRule{
132-
Identifier: "valid-asset-discord-notification",
133-
Fast: true,
134-
Severity: ValidatorSeverityCritical,
135-
AssetValidator: EnsureDiscordFieldInAssetIsValid,
107+
AssetValidator: EnsureAssetNotificationsAreValid,
136108
ApplicableLevels: []Level{LevelAsset},
137109
},
138110
&SimpleRule{

pkg/lint/rules.go

Lines changed: 65 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,12 @@ const (
4646

4747
pipelineContainsCycle = "The pipeline has a cycle with dependencies, make sure there are no cyclic dependencies"
4848

49-
pipelineSlackFieldEmptyChannel = "Slack notifications must have a `channel` attribute"
50-
pipelineSlackChannelFieldNotUnique = "The `channel` attribute under the Slack notifications must be unique"
51-
52-
pipelineMSTeamsConnectionFieldNotUnique = "The `connection` attribute under the MS Teams notifications must be unique"
53-
pipelineMSTeamsConnectionFieldEmpty = "MS Teams notifications `connection` attribute must not be empty"
54-
55-
pipelineDiscordConnectionFieldEmpty = "Discord notifications `connection` attribute must not be empty"
56-
pipelineDiscordConnectionFieldNotUnique = "The `connection` attribute under the Discord notifications must be unique"
57-
58-
assetSlackFieldEmptyChannel = "Asset-level Slack notifications must have a `channel` attribute"
59-
assetSlackChannelFieldNotUnique = "The `channel` attribute under the asset-level Slack notifications must be unique"
60-
assetMSTeamsConnectionFieldEmpty = "Asset-level MS Teams notifications `connection` attribute must not be empty"
61-
assetMSTeamsConnectionFieldNotUnique = "The `connection` attribute under the asset-level MS Teams notifications must be unique"
62-
assetDiscordConnectionFieldEmpty = "Asset-level Discord notifications `connection` attribute must not be empty"
63-
assetDiscordConnectionFieldNotUnique = "The `connection` attribute under the asset-level Discord notifications must be unique"
49+
slackChannelEmpty = "Slack notifications must have a `channel` attribute"
50+
slackChannelNotUnique = "The `channel` attribute under the Slack notifications must be unique"
51+
msTeamsConnectionEmpty = "MS Teams notifications `connection` attribute must not be empty"
52+
msTeamsConnectionNotUnique = "The `connection` attribute under the MS Teams notifications must be unique"
53+
discordConnectionEmpty = "Discord notifications `connection` attribute must not be empty"
54+
discordConnectionNotUnique = "The `connection` attribute under the Discord notifications must be unique"
6455

6556
pipelineConcurrencyMustBePositive = "Pipeline concurrency must be 1 or greater"
6657
pipelineMaxActiveStepsMustBePositive = "Pipeline max_active_steps must be a positive number"
@@ -931,170 +922,95 @@ func EnsurePipelineHasNoCycles(ctx context.Context, p *pipeline.Pipeline) ([]*Is
931922
return issues, nil
932923
}
933924

934-
func isStringInArray(arr []string, str string) bool {
935-
for _, a := range arr {
936-
if str == a {
937-
return true
938-
}
925+
// validateNotifications checks a single Notifications value for empty/duplicate targets.
926+
// issueContext is an optional label (e.g. "custom_check:my_check") included in issues for traceability.
927+
func validateNotifications(n *pipeline.Notifications, issueContext []string) []*Issue {
928+
if n == nil {
929+
return nil
939930
}
940-
return false
941-
}
942931

943-
func EnsureSlackFieldInPipelineIsValid(ctx context.Context, p *pipeline.Pipeline) ([]*Issue, error) {
944-
issues := make([]*Issue, 0)
932+
var issues []*Issue
945933

946-
slackChannels := make([]string, 0, len(p.Notifications.Slack))
947-
for _, slack := range p.Notifications.Slack {
948-
channelWithoutHash := strings.TrimPrefix(slack.Channel, "#")
949-
if channelWithoutHash == "" {
950-
issues = append(issues, &Issue{
951-
Description: pipelineSlackFieldEmptyChannel,
952-
})
934+
slackChannels := make([]string, 0, len(n.Slack))
935+
for _, s := range n.Slack {
936+
ch := strings.TrimPrefix(s.Channel, "#")
937+
if ch == "" {
938+
issues = append(issues, &Issue{Description: slackChannelEmpty, Context: issueContext})
953939
continue
954940
}
955-
956-
if isStringInArray(slackChannels, channelWithoutHash) {
957-
issues = append(issues, &Issue{
958-
Description: pipelineSlackChannelFieldNotUnique,
959-
})
941+
if slices.Contains(slackChannels, ch) {
942+
issues = append(issues, &Issue{Description: slackChannelNotUnique, Context: issueContext})
960943
}
961-
962-
slackChannels = append(slackChannels, channelWithoutHash)
944+
slackChannels = append(slackChannels, ch)
963945
}
964946

965-
return issues, nil
966-
}
967-
968-
func EnsureMSTeamsFieldInPipelineIsValid(ctx context.Context, p *pipeline.Pipeline) ([]*Issue, error) {
969-
issues := make([]*Issue, 0)
970-
971-
MSTeamsConnections := make([]string, 0, len(p.Notifications.MSTeams))
972-
for _, notification := range p.Notifications.MSTeams {
973-
if notification.Connection == "" {
974-
issues = append(issues, &Issue{
975-
Description: pipelineMSTeamsConnectionFieldEmpty,
976-
})
977-
continue
978-
}
947+
issues = append(issues, validateConnectionKeys(
948+
mapSlice(n.MSTeams, func(t pipeline.MSTeamsNotification) string { return t.Connection }),
949+
msTeamsConnectionEmpty, msTeamsConnectionNotUnique, issueContext,
950+
)...)
951+
issues = append(issues, validateConnectionKeys(
952+
mapSlice(n.Discord, func(d pipeline.DiscordNotification) string { return d.Connection }),
953+
discordConnectionEmpty, discordConnectionNotUnique, issueContext,
954+
)...)
979955

980-
if isStringInArray(MSTeamsConnections, notification.Connection) {
981-
issues = append(issues, &Issue{
982-
Description: pipelineMSTeamsConnectionFieldNotUnique,
983-
})
984-
}
985-
986-
MSTeamsConnections = append(MSTeamsConnections, notification.Connection)
987-
}
988-
989-
return issues, nil
956+
return issues
990957
}
991958

992-
func EnsureDiscordFieldInPipelineIsValid(ctx context.Context, p *pipeline.Pipeline) ([]*Issue, error) {
993-
issues := make([]*Issue, 0)
994-
995-
discordConnections := make([]string, 0, len(p.Notifications.Discord))
996-
for _, notification := range p.Notifications.Discord {
997-
if notification.Connection == "" {
998-
issues = append(issues, &Issue{
999-
Description: pipelineDiscordConnectionFieldEmpty,
1000-
})
1001-
continue
1002-
}
1003-
1004-
if isStringInArray(discordConnections, notification.Connection) {
1005-
issues = append(issues, &Issue{
1006-
Description: pipelineDiscordConnectionFieldNotUnique,
1007-
})
1008-
}
1009-
1010-
discordConnections = append(discordConnections, notification.Connection)
959+
func mapSlice[T any](items []T, key func(T) string) []string {
960+
out := make([]string, len(items))
961+
for i, item := range items {
962+
out[i] = key(item)
1011963
}
1012-
1013-
return issues, nil
964+
return out
1014965
}
1015966

1016-
func EnsureSlackFieldInAssetIsValid(ctx context.Context, p *pipeline.Pipeline, asset *pipeline.Asset) ([]*Issue, error) {
1017-
issues := make([]*Issue, 0)
1018-
1019-
if asset.Notifications == nil {
1020-
return issues, nil
1021-
}
1022-
1023-
slackChannels := make([]string, 0, len(asset.Notifications.Slack))
1024-
for _, slack := range asset.Notifications.Slack {
1025-
channelWithoutHash := strings.TrimPrefix(slack.Channel, "#")
1026-
if channelWithoutHash == "" {
1027-
issues = append(issues, &Issue{
1028-
Description: assetSlackFieldEmptyChannel,
1029-
})
967+
// validateConnectionKeys checks a list of connection keys for empty/duplicate values.
968+
func validateConnectionKeys(keys []string, emptyMsg, dupeMsg string, issueContext []string) []*Issue {
969+
var issues []*Issue
970+
var seen []string
971+
for _, k := range keys {
972+
if k == "" {
973+
issues = append(issues, &Issue{Description: emptyMsg, Context: issueContext})
1030974
continue
1031975
}
1032-
1033-
if isStringInArray(slackChannels, channelWithoutHash) {
1034-
issues = append(issues, &Issue{
1035-
Description: assetSlackChannelFieldNotUnique,
1036-
})
976+
if slices.Contains(seen, k) {
977+
issues = append(issues, &Issue{Description: dupeMsg, Context: issueContext})
978+
continue
1037979
}
1038-
1039-
slackChannels = append(slackChannels, channelWithoutHash)
980+
seen = append(seen, k)
1040981
}
1041-
1042-
return issues, nil
982+
return issues
1043983
}
1044984

1045-
func EnsureMSTeamsFieldInAssetIsValid(ctx context.Context, p *pipeline.Pipeline, asset *pipeline.Asset) ([]*Issue, error) {
1046-
issues := make([]*Issue, 0)
1047-
1048-
if asset.Notifications == nil {
1049-
return issues, nil
1050-
}
1051-
1052-
MSTeamsConnections := make([]string, 0, len(asset.Notifications.MSTeams))
1053-
for _, notification := range asset.Notifications.MSTeams {
1054-
if notification.Connection == "" {
1055-
issues = append(issues, &Issue{
1056-
Description: assetMSTeamsConnectionFieldEmpty,
1057-
})
1058-
continue
1059-
}
1060-
1061-
if isStringInArray(MSTeamsConnections, notification.Connection) {
1062-
issues = append(issues, &Issue{
1063-
Description: assetMSTeamsConnectionFieldNotUnique,
1064-
})
1065-
}
1066-
1067-
MSTeamsConnections = append(MSTeamsConnections, notification.Connection)
985+
// EnsurePipelineNotificationsAreValid validates all notification targets on the pipeline.
986+
func EnsurePipelineNotificationsAreValid(ctx context.Context, p *pipeline.Pipeline) ([]*Issue, error) {
987+
issues := validateNotifications(&p.Notifications, nil)
988+
if issues == nil {
989+
return []*Issue{}, nil
1068990
}
1069-
1070991
return issues, nil
1071992
}
1072993

1073-
func EnsureDiscordFieldInAssetIsValid(ctx context.Context, p *pipeline.Pipeline, asset *pipeline.Asset) ([]*Issue, error) {
1074-
issues := make([]*Issue, 0)
994+
// EnsureAssetNotificationsAreValid validates notification targets on the asset,
995+
// its custom checks, and its column checks.
996+
func EnsureAssetNotificationsAreValid(ctx context.Context, p *pipeline.Pipeline, asset *pipeline.Asset) ([]*Issue, error) {
997+
var issues []*Issue
1075998

1076-
if asset.Notifications == nil {
1077-
return issues, nil
1078-
}
999+
issues = append(issues, validateNotifications(asset.Notifications, nil)...)
10791000

1080-
discordConnections := make([]string, 0, len(asset.Notifications.Discord))
1081-
for _, notification := range asset.Notifications.Discord {
1082-
if notification.Connection == "" {
1083-
issues = append(issues, &Issue{
1084-
Description: assetDiscordConnectionFieldEmpty,
1085-
})
1086-
continue
1087-
}
1001+
for _, check := range asset.CustomChecks {
1002+
issues = append(issues, validateNotifications(check.Notifications, []string{"custom_check:" + check.Name})...)
1003+
}
10881004

1089-
if isStringInArray(discordConnections, notification.Connection) {
1090-
issues = append(issues, &Issue{
1091-
Description: assetDiscordConnectionFieldNotUnique,
1092-
})
1005+
for _, col := range asset.Columns {
1006+
for _, check := range col.Checks {
1007+
issues = append(issues, validateNotifications(check.Notifications, []string{col.Name + "." + check.Name})...)
10931008
}
1094-
1095-
discordConnections = append(discordConnections, notification.Connection)
10961009
}
10971010

1011+
if len(issues) == 0 {
1012+
return []*Issue{}, nil
1013+
}
10981014
return issues, nil
10991015
}
11001016

0 commit comments

Comments
 (0)