cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.55.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/discovery.go

229lines · modecode

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