cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.59.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/discovery.go

251lines · modecode

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