Repository: prometheus/blackbox_exporter
Pull request #552 (2019-11-20 15:17:03)

Add none/all matches to DNS RRset validation
The current validators for the DNS probers (if_matches/if_not_matches) pass if _any_ record matches/does not match. This patch adds the opposite sense of each, so that a validation will pass if _no_ records match, or _all_ records match. This is much more useful for example with TXT records where you might want to check if a particular record is present or not without failing if any record does not match.

brian-brazil: Can you do a make format to make CI happy?
wodity: Updated.
brian-brazil: The configuration docs need updating too.
wodity: Ah, missed that. Updated those too.

Patch:
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
index 7610bd4..93d3bb8 100644
--- a/CONFIGURATION.md
+++ b/CONFIGURATION.md
@@ -165,25 +165,43 @@ validate_answer_rrs:
   fail_if_matches_regexp:
     [ - <regex>, ... ]
 
+  fail_if_all_match_regexp:
+    [ - <regex>, ... ]
+
   fail_if_not_matches_regexp:
     [ - <regex>, ... ]
 
+  fail_if_none_matches_regexp:
+    [ - <regex>, ... ]
+
 validate_authority_rrs:
 
   fail_if_matches_regexp:
     [ - <regex>, ... ]
 
+  fail_if_all_match_regexp:
+    [ - <regex>, ... ]
+
   fail_if_not_matches_regexp:
     [ - <regex>, ... ]
 
+  fail_if_none_matches_regexp:
+    [ - <regex>, ... ]
+
 validate_additional_rrs:
 
   fail_if_matches_regexp:
     [ - <regex>, ... ]
 
+  fail_if_all_match_regexp:
+    [ - <regex>, ... ]
+
   fail_if_not_matches_regexp:
     [ - <regex>, ... ]
 
+  fail_if_none_matches_regexp:
+    [ - <regex>, ... ]
+
 ```
 
 ### <icmp_probe>
diff --git a/config/config.go b/config/config.go
index 3f3daea..148de84 100644
--- a/config/config.go
+++ b/config/config.go
@@ -184,8 +184,10 @@ type DNSProbe struct {
 }
 
 type DNSRRValidator struct {
-	FailIfMatchesRegexp    []string `yaml:"fail_if_matches_regexp,omitempty"`
-	FailIfNotMatchesRegexp []string `yaml:"fail_if_not_matches_regexp,omitempty"`
+	FailIfMatchesRegexp     []string `yaml:"fail_if_matches_regexp,omitempty"`
+	FailIfAllMatchRegexp    []string `yaml:"fail_if_all_match_regexp,omitempty"`
+	FailIfNotMatchesRegexp  []string `yaml:"fail_if_not_matches_regexp,omitempty"`
+	FailIfNoneMatchesRegexp []string `yaml:"fail_if_none_matches_regexp,omitempty"`
 }
 
 // UnmarshalYAML implements the yaml.Unmarshaler interface.
diff --git a/example.yml b/example.yml
index 6c720ff..3416c74 100644
--- a/example.yml
+++ b/example.yml
@@ -112,8 +112,12 @@ modules:
       validate_answer_rrs:
         fail_if_matches_regexp:
         - ".*127.0.0.1"
+        fail_if_all_match_regexp:
+        - ".*127.0.0.1"
         fail_if_not_matches_regexp:
         - "www.prometheus.io.\t300\tIN\tA\t127.0.0.1"
+        fail_if_none_matches_regexp:
+        - "127.0.0.1"
       validate_authority_rrs:
         fail_if_matches_regexp:
         - ".*127.0.0.1"
diff --git a/prober/dns.go b/prober/dns.go
index 00e6467..9c26c9b 100644
--- a/prober/dns.go
+++ b/prober/dns.go
@@ -29,12 +29,18 @@ import (
 
 // validRRs checks a slice of RRs received from the server against a DNSRRValidator.
 func validRRs(rrs *[]dns.RR, v *config.DNSRRValidator, logger log.Logger) bool {
+	var anyMatch bool = false
+	var allMatch bool = true
 	// Fail the probe if there are no RRs of a given type, but a regexp match is required
-	// (i.e. FailIfNotMatchesRegexp is set).
+	// (i.e. FailIfNotMatchesRegexp or FailIfNoneMatchesRegexp is set).
 	if len(*rrs) == 0 && len(v.FailIfNotMatchesRegexp) > 0 {
 		level.Error(logger).Log("msg", "fail_if_not_matches_regexp specified but no RRs returned")
 		return false
 	}
+	if len(*rrs) == 0 && len(v.FailIfNoneMatchesRegexp) > 0 {
+		level.Error(logger).Log("msg", "fail_if_none_matches_regexp specified but no RRs returned")
+		return false
+	}
 	for _, rr := range *rrs {
 		level.Info(logger).Log("msg", "Validating RR", "rr", rr)
 		for _, re := range v.FailIfMatchesRegexp {
@@ -44,10 +50,20 @@ func validRRs(rrs *[]dns.RR, v *config.DNSRRValidator, logger log.Logger) bool {
 				return false
 			}
 			if match {
-				level.Error(logger).Log("msg", "RR matched regexp", "regexp", re, "rr", rr)
+				level.Error(logger).Log("msg", "At least one RR matched regexp", "regexp", re, "rr", rr)
 				return false
 			}
 		}
+		for _, re := range v.FailIfAllMatchRegexp {
+			match, err := regexp.MatchString(re, rr.String())
+			if err != nil {
+				level.Error(logger).Log("msg", "Error matching regexp", "regexp", re, "err", err)
+				return false
+			}
+			if !match {
+				allMatch = false
+			}
+		}
 		for _, re := range v.FailIfNotMatchesRegexp {
 			match, err := regexp.MatchString(re, rr.String())
 			if err != nil {
@@ -55,11 +71,29 @@ func validRRs(rrs *[]dns.RR, v *config.DNSRRValidator, logger log.Logger) bool {
 				return false
 			}
 			if !match {
-				level.Error(logger).Log("msg", "RR did not match regexp", "regexp", re, "rr", rr)
+				level.Error(logger).Log("msg", "At least one RR did not match regexp", "regexp", re, "rr", rr)
+				return false
+			}
+		}
+		for _, re := range v.FailIfNoneMatchesRegexp {
+			match, err := regexp.MatchString(re, rr.String())
+			if err != nil {
+				level.Error(logger).Log("msg", "Error matching regexp", "regexp", re, "err", err)
 				return false
 			}
+			if match {
+				anyMatch = true
+			}
 		}
 	}
+	if len(v.FailIfAllMatchRegexp) > 0 && !allMatch {
+		level.Error(logger).Log("msg", "Not all RRs matched regexp")
+		return false
+	}
+	if len(v.FailIfNoneMatchesRegexp) > 0 && !anyMatch {
+		level.Error(logger).Log("msg", "None of the RRs did matched any regexp")
+		return false
+	}
 	return true
 }
 


Test patch:
diff --git a/prober/dns_test.go b/prober/dns_test.go
index 9b7b4a6..b4f0472 100644
--- a/prober/dns_test.go
+++ b/prober/dns_test.go
@@ -303,6 +303,26 @@ func TestAuthoritativeDNSResponse(t *testing.T) {
 				},
 			}, false,
 		},
+		{
+			config.DNSProbe{
+				IPProtocol:         "ip4",
+				IPProtocolFallback: true,
+				QueryName:          "example.com",
+				ValidateAdditional: config.DNSRRValidator{
+					FailIfAllMatchRegexp: []string{".*127.0.0.*"},
+				},
+			}, false,
+		},
+		{
+			config.DNSProbe{
+				IPProtocol:         "ip4",
+				IPProtocolFallback: true,
+				QueryName:          "example.com",
+				ValidateAdditional: config.DNSRRValidator{
+					FailIfNoneMatchesRegexp: []string{".*127.0.0.3.*"},
+				},
+			}, false,
+		},
 	}
 
 	for _, protocol := range PROTOCOLS {