cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.50.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/discovery/discovery.go

219lines · modecode

1package discovery
2
3import (
4 "encoding/json"
5 "errors"
6 "io"
7 "log/slog"
8 "regexp"
9 "strings"
10 "time"
11
12 "github.com/prometheus/prometheus/model/rulefmt"
13 "golang.org/x/exp/slices"
14
15 "github.com/cloudflare/pint/internal/comments"
16 "github.com/cloudflare/pint/internal/output"
17 "github.com/cloudflare/pint/internal/parser"
18)
19
20const (
21 FileOwnerComment = "file/owner"
22 FileDisabledCheckComment = "file/disable"
23 FileSnoozeCheckComment = "file/snooze"
24 RuleOwnerComment = "rule/owner"
25)
26
27var ignoredErrors = []string{
28 "one of 'record' or 'alert' must be set",
29 "field 'expr' must be set in rule",
30 "could not parse expression: ",
31 "cannot unmarshal !!seq into rulefmt.ruleGroups",
32 ": template: __",
33}
34
35var ErrFileIsIgnored = errors.New("file was ignored")
36
37func isStrictIgnored(err error) bool {
38 s := err.Error()
39 for _, ign := range ignoredErrors {
40 if strings.Contains(s, ign) {
41 return true
42 }
43 }
44 return false
45}
46
47type RuleFinder interface {
48 Find() ([]Entry, error)
49}
50
51type ChangeType int
52
53func (c ChangeType) String() string {
54 switch c {
55 case Unknown:
56 return "unknown"
57 case Noop:
58 return "noop"
59 case Added:
60 return "added"
61 case Modified:
62 return "modified"
63 case Removed:
64 return "removed"
65 case Moved:
66 return "moved"
67 default:
68 return "---"
69 }
70}
71
72func (c *ChangeType) MarshalJSON() ([]byte, error) {
73 return json.Marshal(c.String())
74}
75
76const (
77 Unknown ChangeType = iota
78 Noop
79 Added
80 Modified
81 Removed
82 Moved
83)
84
85type Entry struct {
86 State ChangeType
87 ReportedPath string // symlink target
88 SourcePath string // file path (can be symlink)
89 PathError error
90 ModifiedLines []int
91 Rule parser.Rule
92 Owner string
93 DisabledChecks []string
94}
95
96func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (entries []Entry, err error) {
97 p := parser.NewParser()
98
99 content, fileComments, err := parser.ReadContent(r)
100 if err != nil {
101 return nil, err
102 }
103
104 contentLines := []int{}
105 for i := 1; i <= strings.Count(string(content.Body), "\n"); i++ {
106 contentLines = append(contentLines, i)
107 }
108
109 var fileOwner string
110 var disabledChecks []string
111 for _, comment := range fileComments {
112 // nolint:exhaustive
113 switch comment.Type {
114 case comments.FileOwnerType:
115 owner := comment.Value.(comments.Owner)
116 fileOwner = owner.Name
117 case comments.FileDisableType:
118 disable := comment.Value.(comments.Disable)
119 if !slices.Contains(disabledChecks, disable.Match) {
120 disabledChecks = append(disabledChecks, disable.Match)
121 }
122 case comments.FileSnoozeType:
123 snooze := comment.Value.(comments.Snooze)
124 if !snooze.Until.After(time.Now()) {
125 continue
126 }
127 if !slices.Contains(disabledChecks, snooze.Match) {
128 disabledChecks = append(disabledChecks, snooze.Match)
129 }
130 slog.Debug(
131 "Check snoozed by comment",
132 slog.String("check", snooze.Match),
133 slog.String("match", snooze.Match),
134 slog.Time("until", snooze.Until),
135 )
136 }
137 }
138
139 if content.Ignored {
140 entries = append(entries, Entry{
141 ReportedPath: reportedPath,
142 SourcePath: sourcePath,
143 PathError: ErrFileIsIgnored,
144 Owner: fileOwner,
145 ModifiedLines: contentLines,
146 })
147 return entries, nil
148 }
149
150 if isStrict {
151 if _, errs := rulefmt.Parse(content.Body); len(errs) > 0 {
152 seen := map[string]struct{}{}
153 for _, err := range errs {
154 if isStrictIgnored(err) {
155 continue
156 }
157 if _, ok := seen[err.Error()]; ok {
158 continue
159 }
160 seen[err.Error()] = struct{}{}
161 entries = append(entries, Entry{
162 ReportedPath: reportedPath,
163 SourcePath: sourcePath,
164 PathError: err,
165 Owner: fileOwner,
166 ModifiedLines: contentLines,
167 })
168 }
169 if len(entries) > 0 {
170 return entries, nil
171 }
172 }
173 }
174
175 rules, err := p.Parse(content.Body)
176 if err != nil {
177 slog.Error(
178 "Failed to parse file content",
179 slog.Any("err", err),
180 slog.String("path", sourcePath),
181 slog.String("lines", output.FormatLineRangeString(contentLines)),
182 )
183 entries = append(entries, Entry{
184 ReportedPath: reportedPath,
185 SourcePath: sourcePath,
186 PathError: err,
187 Owner: fileOwner,
188 ModifiedLines: contentLines,
189 })
190 return entries, nil
191 }
192
193 for _, rule := range rules {
194 ruleOwner := fileOwner
195 for _, owner := range comments.Only[comments.Owner](rule.Comments, comments.RuleOwnerType) {
196 ruleOwner = owner.Name
197 }
198 entries = append(entries, Entry{
199 ReportedPath: reportedPath,
200 SourcePath: sourcePath,
201 Rule: rule,
202 ModifiedLines: rule.Lines(),
203 Owner: ruleOwner,
204 DisabledChecks: disabledChecks,
205 })
206 }
207
208 slog.Debug("File parsed", slog.String("path", sourcePath), slog.Int("rules", len(entries)))
209 return entries, nil
210}
211
212func matchesAny(re []*regexp.Regexp, s string) bool {
213 for _, r := range re {
214 if v := r.MatchString(s); v {
215 return true
216 }
217 }
218 return false
219}
220