Skip to content

Commit

Permalink
HIP-0019 adds .helmlintignore capability
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
dpritchett committed Aug 16, 2024
1 parent c86e0d3 commit f93c255
Show file tree
Hide file tree
Showing 5 changed files with 517 additions and 0 deletions.
24 changes: 24 additions & 0 deletions pkg/lint/ignore/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

//Package ignore
/*
Package ignore contains tools for linting charts.
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.
*/
package ignore // import "helm.sh/helm/v3/pkg/lint/ignore"
62 changes: 62 additions & 0 deletions pkg/lint/ignore/ignorer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ignore

import (
"fmt"
"helm.sh/helm/v3/pkg/lint/support"
"log"
)

// DefaultIgnoreFileName is the name of the lint ignore file
const DefaultIgnoreFileName = ".helmlintignore"

var debugFn func(format string, v ...interface{})

type Ignorer struct {
ChartPath string
Matchers []MatchesErrors
}

func NewIgnorer(chartPath string, lintIgnorePath string, debugLogFn func(string, ...interface{})) (*Ignorer, error) {
debugFn = debugLogFn
matchers, err := LoadFromFilePath(chartPath, lintIgnorePath)
if err != nil {
return nil, err
}

return &Ignorer{ChartPath: chartPath, Matchers: matchers}, nil
}

// FilterMessages Verify what messages can be kept in the output, using also the error as a verification
func (i *Ignorer) FilterMessages(messages []support.Message) []support.Message {
out := make([]support.Message, 0, len(messages))
for _, msg := range messages {
if i.ShouldKeepMessage(msg) {
out = append(out, msg)
}
}
return out
}

func (i *Ignorer) ShouldKeepMessage(msg support.Message) bool {
return i.ShouldKeepError(msg.Err)
}

// 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
func (i *Ignorer) ShouldKeepError(err error) bool {
// if any of our Matchers match the rule, we can discard it
for _, rule := range i.Matchers {
if rule.Match(err) {
debug("lint ignore rule matched this error, we should suppress it.", "errText", err.Error(), rule.LogAttrs())
return false
}
}

// if we can't find a reason to discard it, we keep it
debug("no lint ignore rules matched this error, we should NOT suppress it.", "errText", err.Error())
return true
}

var defaultDebugFn = func(format string, v ...interface{}) {
format = fmt.Sprintf("[debug] %s\n", format)
_ = log.Output(2, fmt.Sprintf(format, v...))
}
78 changes: 78 additions & 0 deletions pkg/lint/ignore/matchers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package ignore

import (
"log/slog"
"path/filepath"
"strings"
)

const pathlessRulePrefix = "error_lint_ignore="

type MatchesErrors interface {
Match(error) bool
LogAttrs() slog.Attr
}

type BadTemplateRule struct {
RuleText string
MessageText string
BadTemplatePath string
}

type PathlessRule struct {
RuleText string
MessageText string
}

// Match errors that have no file path in their body with ignorer rules.
// An examples of errors with no file path in their body is chart metadata errors `chart metadata is missing these dependencies`
func (pr PathlessRule) Match(err error) bool {
errText := err.Error()
matchableParts := strings.SplitN(pr.MessageText, ":", 2)
matchablePrefix := strings.TrimSpace(matchableParts[0])

if match, _ := filepath.Match(pr.MessageText, errText); match {
return true
}
if matched, _ := filepath.Match(matchablePrefix, errText); matched {
return true
}

return false
}

// LogAttrs Used for troubleshooting and gathering data
func (pr PathlessRule) LogAttrs() slog.Attr {
return slog.Group("BadTemplateRule",
slog.String("rule_text", pr.RuleText),
slog.String("value", pr.MessageText),
)
}

// LogAttrs Used for troubleshooting and gathering data
func (btr BadTemplateRule) LogAttrs() slog.Attr {
return slog.Group("BadTemplateRule",
slog.String("rule_text", btr.RuleText),
slog.String("key", btr.BadTemplatePath),
slog.String("value", btr.MessageText),
)
}

// Match errors that have a file path in their body with ignorer rules.
func (btr BadTemplateRule) Match(err error) bool {
errText := err.Error()
pathToOffendingFile, err := pathToOffendingFile(errText)
if err != nil {
return false
}

cleanRulePath := filepath.Clean(btr.BadTemplatePath)

if strings.Contains(pathToOffendingFile, cleanRulePath) {
if strings.Contains(errText, btr.MessageText) {
return true
}
}

return false
}
113 changes: 113 additions & 0 deletions pkg/lint/ignore/rule_loaders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package ignore

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)

func LoadFromFilePath(chartPath, ignoreFilePath string) ([]MatchesErrors, error) {
if ignoreFilePath == "" {
ignoreFilePath = filepath.Join(chartPath, DefaultIgnoreFileName)
debug("\nNo helm lint ignore filepath specified, will try and use the following default: %s\n", ignoreFilePath)
}

// attempt to load ignore patterns from ignoreFilePath.
// if none are found, return an empty ignorer so the program can keep running.
debug("\nTrying to load helm lint ignore file at %s\n", ignoreFilePath)
file, err := os.Open(ignoreFilePath)
if err != nil {
debug("failed to open helm lint ignore file: %s", ignoreFilePath)
return []MatchesErrors{}, nil
}
defer func() {
err := file.Close()
if err != nil {
debug("failed to close helm lint ignore file: %s", ignoreFilePath)
}
}()

matchers := LoadFromReader(file)
return matchers, nil
}

// debug provides [pkg/lint/ignore] with a runtime-overridable logging function
// intended to match the behavior of the top level debug() method from package main.
//
// When no debugFn is set for the package at runtime then debug will fall back to
// defaultDebugFn.
func debug(format string, args ...interface{}) {
if debugFn == nil {
defaultDebugFn(format, args...)
} else {
debugFn(format, args...)
}
return
}

// TODO: figure out & fix or remove
func pathToOffendingFile(errText string) (string, error) {
delimiter := ":"
// splits into N parts delimited by colons
parts := strings.Split(errText, delimiter)
// if 3 or more parts, return the second part, after trimming its spaces
if len(parts) > 2 {
return strings.TrimSpace(parts[1]), nil
}
// if fewer than 3 parts, return empty string
return "", fmt.Errorf("fewer than three [%s]-delimited parts found, no path here: %s", delimiter, errText)
}

func LoadFromReader(rdr io.Reader) []MatchesErrors {
matchers := make([]MatchesErrors, 0)

scanner := bufio.NewScanner(rdr)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}

if strings.HasPrefix(line, pathlessRulePrefix) {
matchers = append(matchers, buildPathlessRule(line, pathlessRulePrefix))
} else {
matchers = append(matchers, buildBadTemplateRule(line))
}
}

return matchers
}

func buildPathlessRule(line string, pathlessRulePrefix string) PathlessRule {
// handle chart-level errors
// Drop 'error_lint_ignore=' prefix from rule before saving it
const numSplits = 2
tokens := strings.SplitN(line[len(pathlessRulePrefix):], pathlessRulePrefix, numSplits)
if len(tokens) == numSplits {
// TODO: find an example for this one - not sure we still use it
messageText, _ := tokens[0], tokens[1]
return PathlessRule{RuleText: line, MessageText: messageText}
} else {
messageText := tokens[0]
return PathlessRule{RuleText: line, MessageText: messageText}
}
}

func buildBadTemplateRule(line string) BadTemplateRule {
const noMessageText = ""
const separator = " "
const numSplits = 2

// handle chart yaml file errors in specific template files
parts := strings.SplitN(line, separator, numSplits)
if len(parts) == numSplits {
messagePath, messageText := parts[0], parts[1]
return BadTemplateRule{RuleText: line, BadTemplatePath: messagePath, MessageText: messageText}
} else {
messagePath := parts[0]
return BadTemplateRule{RuleText: line, BadTemplatePath: messagePath, MessageText: noMessageText}
}
}
Loading

0 comments on commit f93c255

Please sign in to comment.