Skip to content

Commit c64d08e

Browse files
committed
HIP-0019 adds .helmlintignore capability
See HIP-0019 proposal at helm/community: helm/community#353 Co-authored-by: Danilo Patrucco <[email protected]> Signed-off-by: Daniel J. Pritchett <[email protected]>
1 parent c86e0d3 commit c64d08e

File tree

6 files changed

+576
-0
lines changed

6 files changed

+576
-0
lines changed

pkg/lint/ignore/doc.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright The Helm Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
//Package ignore
18+
/*
19+
Package ignore contains tools for linting charts.
20+
21+
Linting is the process of testing charts for errors or warnings regarding
22+
formatting, compilation, or standards compliance.
23+
*/
24+
package ignore // import "helm.sh/helm/v3/pkg/lint/ignore"

pkg/lint/ignore/ignorer.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package ignore
2+
3+
import (
4+
"helm.sh/helm/v3/pkg/lint/support"
5+
"log/slog"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
type Ignorer struct {
11+
ChartPath string
12+
Rules []Rule
13+
logger *slog.Logger
14+
RuleLoader *RuleLoader
15+
}
16+
17+
type PathlessRule struct {
18+
RuleText string
19+
MessageText string
20+
}
21+
22+
// Ignorer is used to create the ignorer object that contains the ignore rules
23+
func NewActionIgnorer(chartPath string, lintIgnorePath string, debugLogFn func(string, ...interface{})) (*Ignorer, error) {
24+
cmdIgnorer, err := NewRuleLoader(chartPath, lintIgnorePath, debugLogFn)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
return &Ignorer{ChartPath: chartPath, RuleLoader: cmdIgnorer}, nil
30+
}
31+
32+
// FilterMessages Verify what messages can be kept in the output, using also the error as a verification (calling ShouldKeepError)
33+
func (i *Ignorer) FilterMessages(messages []support.Message) []support.Message {
34+
out := make([]support.Message, 0, len(messages))
35+
for _, msg := range messages {
36+
if i.ShouldKeepError(msg.Err) {
37+
out = append(out, msg)
38+
}
39+
}
40+
return out
41+
}
42+
43+
// ShouldKeepError is used to verify if the error associated with the message need to be kept, or it can be ignored, called by FilterMessages and in the pkg/action/lint.go Run main function
44+
func (i *Ignorer) ShouldKeepError(err error) bool {
45+
errText := err.Error()
46+
47+
// if any of our Matchers match the rule, we can discard it
48+
for _, rule := range i.RuleLoader.Matchers {
49+
match := rule.Match(errText)
50+
if match != nil {
51+
i.RuleLoader.Debug("lint ignore rule matched", match.LogAttrs())
52+
return false
53+
}
54+
}
55+
56+
// if we can't find a reason to discard it, we keep it
57+
return true
58+
}
59+
60+
type MatchesErrors interface {
61+
Match(string) *RuleMatch
62+
}
63+
64+
type RuleMatch struct {
65+
ErrText string
66+
RuleText string
67+
}
68+
69+
func (rm RuleMatch) LogAttrs() slog.Attr {
70+
return slog.Group("rule_match", slog.String("err_text", rm.ErrText), slog.String("rule_text", rm.RuleText))
71+
}
72+
73+
// Match errors that have no file path in their body with ignorer rules.
74+
// An examples of errors with no file path in their body is chart metadata errors `chart metadata is missing these dependencies`
75+
func (pr PathlessRule) Match(errText string) *RuleMatch {
76+
ignorableError := pr.MessageText
77+
parts := strings.SplitN(ignorableError, ":", 2)
78+
prefix := strings.TrimSpace(parts[0])
79+
80+
if match, _ := filepath.Match(ignorableError, errText); match {
81+
return &RuleMatch{ErrText: errText, RuleText: pr.RuleText}
82+
}
83+
84+
if matched, _ := filepath.Match(prefix, errText); matched {
85+
return &RuleMatch{ErrText: errText, RuleText: pr.RuleText}
86+
}
87+
88+
return nil
89+
}

pkg/lint/ignore/rule.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package ignore
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
type Rule struct {
11+
RuleText string
12+
MessagePath string
13+
MessageText string
14+
}
15+
16+
type LintedMessage struct {
17+
ChartPath string
18+
MessagePath string
19+
MessageText string
20+
}
21+
22+
func NewRule(ruleText string) *Rule {
23+
return &Rule{RuleText: ruleText}
24+
}
25+
26+
// ShouldKeepLintedMessage Function used to test the test data in rule_test.go and verify that the ignore capability work as needed
27+
func (r Rule) ShouldKeepLintedMessage(msg LintedMessage) bool {
28+
cmdIgnorer := RuleLoader{}
29+
rdr := strings.NewReader(r.RuleText)
30+
cmdIgnorer.LoadFromReader(rdr)
31+
32+
actionIgnorer := Ignorer{RuleLoader: &cmdIgnorer}
33+
return actionIgnorer.ShouldKeepError(fmt.Errorf(msg.MessageText))
34+
}
35+
36+
// LogAttrs Used for troubleshooting and gathering data
37+
func (r Rule) LogAttrs() slog.Attr {
38+
return slog.Group("Rule",
39+
slog.String("rule_text", r.RuleText),
40+
slog.String("key", r.MessagePath),
41+
slog.String("value", r.MessageText),
42+
)
43+
}
44+
45+
// Match errors that have a file path in their body with ignorer rules.
46+
// Ignorer rules are built from the lint ignore file
47+
func (r Rule) Match(errText string) *RuleMatch {
48+
errorFullPath, err := extractFullPathFromError(errText)
49+
if err != nil {
50+
return nil
51+
}
52+
53+
ignorablePath := r.MessagePath
54+
ignorableText := r.MessageText
55+
cleanIgnorablePath := filepath.Clean(ignorablePath)
56+
57+
if strings.Contains(errorFullPath, cleanIgnorablePath) {
58+
if strings.Contains(errText, ignorableText) {
59+
return &RuleMatch{ErrText: errText, RuleText: r.RuleText}
60+
}
61+
}
62+
63+
return nil
64+
}

pkg/lint/ignore/rule_loader.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package ignore
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"log"
8+
"log/slog"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
)
13+
14+
// RuleLoader provides a means of suppressing unwanted helm lint errors and messages
15+
// by comparing them to an ignore list provided in a plaintext helm lint ignore file.
16+
type RuleLoader struct {
17+
Matchers []MatchesErrors
18+
debugFnOverride func(string, ...interface{})
19+
}
20+
21+
func (i *RuleLoader) LogAttrs() slog.Attr {
22+
return slog.Group("RuleLoader",
23+
slog.String("Matchers", fmt.Sprintf("%v", i.Matchers)),
24+
)
25+
}
26+
27+
// DefaultIgnoreFileName is the name of the lint ignore file
28+
// an RuleLoader will seek out at load/parse time.
29+
const DefaultIgnoreFileName = ".helmlintignore"
30+
31+
const NoMessageText = ""
32+
33+
// NewRuleLoader builds an RuleLoader object that enables helm to discard specific lint result Messages
34+
// and Errors should they match the ignore rules in the specified .helmlintignore file.
35+
func NewRuleLoader(chartPath, ignoreFilePath string, debugLogFn func(string, ...interface{})) (*RuleLoader, error) {
36+
out := &RuleLoader{
37+
debugFnOverride: debugLogFn,
38+
}
39+
40+
if ignoreFilePath == "" {
41+
ignoreFilePath = filepath.Join(chartPath, DefaultIgnoreFileName)
42+
out.Debug("\nNo HelmLintIgnore file specified, will try and use the following: %s\n", ignoreFilePath)
43+
}
44+
45+
// attempt to load ignore patterns from ignoreFilePath.
46+
// if none are found, return an empty ignorer so the program can keep running.
47+
out.Debug("\nUsing ignore file: %s\n", ignoreFilePath)
48+
file, err := os.Open(ignoreFilePath)
49+
if err != nil {
50+
out.Debug("failed to open lint ignore file: %s", ignoreFilePath)
51+
return out, nil
52+
}
53+
defer file.Close()
54+
55+
out.LoadFromReader(file)
56+
out.Debug("RuleLoader loaded.", out.LogAttrs())
57+
return out, nil
58+
}
59+
60+
// Debug provides an RuleLoader with a caller-overridable logging function
61+
// intended to match the behavior of the top level debug() method from package main.
62+
//
63+
// When no i.debugFnOverride is present Debug will fall back to a naive
64+
// implementation that assumes all debug output should be logged and not swallowed.
65+
func (i *RuleLoader) Debug(format string, args ...interface{}) {
66+
if i.debugFnOverride == nil {
67+
i.debugFnOverride = func(format string, v ...interface{}) {
68+
format = fmt.Sprintf("[debug] %s\n", format)
69+
log.Output(2, fmt.Sprintf(format, v...))
70+
}
71+
}
72+
73+
i.debugFnOverride(format, args...)
74+
}
75+
76+
// TODO: figure out & fix or remove
77+
func extractFullPathFromError(errText string) (string, error) {
78+
delimiter := ":"
79+
// splits into N parts delimited by colons
80+
parts := strings.Split(errText, delimiter)
81+
// if 3 or more parts, return the second part, after trimming its spaces
82+
if len(parts) > 2 {
83+
return strings.TrimSpace(parts[1]), nil
84+
}
85+
// if fewer than 3 parts, return empty string
86+
return "", fmt.Errorf("fewer than three [%s]-delimited parts found, no path here: %s", delimiter, errText)
87+
}
88+
89+
func (i *RuleLoader) LoadFromReader(rdr io.Reader) {
90+
const pathlessPatternPrefix = "error_lint_ignore="
91+
scanner := bufio.NewScanner(rdr)
92+
for scanner.Scan() {
93+
line := strings.TrimSpace(scanner.Text())
94+
if line == "" || strings.HasPrefix(line, "#") {
95+
continue
96+
}
97+
98+
isPathlessPattern := strings.HasPrefix(line, pathlessPatternPrefix)
99+
100+
if isPathlessPattern {
101+
i.storePathlessPattern(line, pathlessPatternPrefix)
102+
} else {
103+
i.storePathfulPattern(line)
104+
}
105+
}
106+
}
107+
108+
func (i *RuleLoader) storePathlessPattern(line string, pathlessPatternPrefix string) {
109+
// handle chart-level errors
110+
// Drop 'error_lint_ignore=' prefix from rule before saving it
111+
const numSplits = 2
112+
tokens := strings.SplitN(line[len(pathlessPatternPrefix):], pathlessPatternPrefix, numSplits)
113+
if len(tokens) == numSplits {
114+
// TODO: find an example for this one - not sure we still use it
115+
messageText, _ := tokens[0], tokens[1]
116+
i.Matchers = append(i.Matchers, PathlessRule{RuleText: line, MessageText: messageText})
117+
} else {
118+
messageText := tokens[0]
119+
i.Matchers = append(i.Matchers, PathlessRule{RuleText: line, MessageText: messageText})
120+
}
121+
}
122+
123+
func (i *RuleLoader) storePathfulPattern(line string) {
124+
const separator = " "
125+
const numSplits = 2
126+
127+
// handle chart yaml file errors in specific template files
128+
parts := strings.SplitN(line, separator, numSplits)
129+
if len(parts) == numSplits {
130+
messagePath, messageText := parts[0], parts[1]
131+
i.Matchers = append(i.Matchers, Rule{RuleText: line, MessagePath: messagePath, MessageText: messageText})
132+
} else {
133+
messagePath := parts[0]
134+
i.Matchers = append(i.Matchers, Rule{RuleText: line, MessagePath: messagePath, MessageText: NoMessageText})
135+
}
136+
}
137+
138+
func (i *RuleLoader) loadFromFilePath(filePath string) {
139+
file, err := os.Open(filePath)
140+
if err != nil {
141+
i.Debug("failed to open lint ignore file: %s", filePath)
142+
return
143+
}
144+
defer file.Close()
145+
i.LoadFromReader(file)
146+
}

pkg/lint/ignore/rule_loader_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package ignore
2+
3+
import (
4+
"fmt"
5+
"github.com/stretchr/testify/assert"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestNewIgnorer(t *testing.T) {
11+
chartPath := "../rules/testdata/withsubchartlintignore"
12+
ignoreFilePath := filepath.Join(chartPath, ".helmlintignore")
13+
ignorer, err := NewRuleLoader(chartPath, ignoreFilePath, func(format string, args ...interface{}) {
14+
t.Logf(format, args...)
15+
})
16+
assert.NoError(t, err)
17+
assert.NotNil(t, ignorer, "RuleLoader should not be nil")
18+
}
19+
20+
func TestDebug(t *testing.T) {
21+
var captured string
22+
debugFn := func(format string, args ...interface{}) {
23+
captured = fmt.Sprintf(format, args...)
24+
}
25+
ignorer := &RuleLoader{
26+
debugFnOverride: debugFn,
27+
}
28+
ignorer.Debug("test %s", "debug")
29+
assert.Equal(t, "test debug", captured)
30+
}

0 commit comments

Comments
 (0)