@ -23,6 +23,9 @@ import (
"path/filepath"
"strings"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -33,6 +36,8 @@ import (
"helm.sh/helm/v3/pkg/lint/support"
)
const globalKey = "global"
var longLintHelp = `
This command takes a path to a chart and runs a series of tests to verify that
the chart is well - formed .
@ -42,6 +47,16 @@ it will emit [ERROR] messages. If it encounters issues that break with conventio
or recommendation , it will emit [ WARNING ] messages .
`
type scopedChart struct {
path string
root bool
}
type parentScope struct {
metadata * chart . Metadata
values map [ string ] interface { }
}
func newLintCmd ( out io . Writer ) * cobra . Command {
client := action . NewLint ( )
valueOpts := & values . Options { }
@ -52,9 +67,13 @@ func newLintCmd(out io.Writer) *cobra.Command {
Short : "examine a chart for possible issues" ,
Long : longLintHelp ,
RunE : func ( _ * cobra . Command , args [ ] string ) error {
paths := [ ] string { "." }
if len ( args ) > 1 {
return fmt . Errorf ( "invalid call, expected path to a single chart, got: %s" , args )
}
charts := [ ] scopedChart { { "." , true } }
root := & parentScope { }
if len ( args ) > 0 {
paths = args
charts[ 0 ] . path = args [ 0 ]
}
if kubeVersion != "" {
@ -64,76 +83,82 @@ func newLintCmd(out io.Writer) *cobra.Command {
}
client . KubeVersion = parsedKubeVersion
}
if client . WithSubcharts {
for _ , p := range paths {
filepath . Walk ( filepath . Join ( p , "charts" ) , func ( path string , info os . FileInfo , _ error ) error {
if info != nil {
if info . Name ( ) == "Chart.yaml" {
paths = append ( paths , filepath . Dir ( path ) )
} else if strings . HasSuffix ( path , ".tgz" ) || strings . HasSuffix ( path , ".tar.gz" ) {
paths = append ( paths , path )
}
}
return nil
} )
}
}
client . Namespace = settings . Namespace ( )
vals , err := valueOpts . MergeValues ( getter . All ( settings ) )
if err != nil {
return err
}
if client . WithSubcharts {
// load root chart metadata & chart values to prepare values for sub-charts
root . metadata , _ = chartutil . LoadChartfile ( filepath . Join ( charts [ 0 ] . path , "Chart.yaml" ) )
root . values , _ = chartutil . ReadValuesFile ( filepath . Join ( charts [ 0 ] . path , "values.yaml" ) )
root . values = chartutil . CoalesceTables ( root . values , vals )
filepath . Walk ( filepath . Join ( charts [ 0 ] . path , "charts" ) , func ( path string , info os . FileInfo , _ error ) error {
if info != nil {
if info . Name ( ) == "Chart.yaml" {
charts = append ( charts , scopedChart { filepath . Dir ( path ) , false } )
} else if strings . HasSuffix ( path , ".tgz" ) || strings . HasSuffix ( path , ".tar.gz" ) {
charts = append ( charts , scopedChart { path , false } )
}
}
return nil
} )
}
var message strings . Builder
failed := 0
errorsOrWarnings := 0
linted := 0
for _ , single := range charts {
for scope , scopedVals := range getScopedValues ( root , single , vals ) {
linted ++
result := client . Run ( [ ] string { single . path } , scopedVals )
// If there is no errors/warnings and quiet flag is set
// go to the next chart
hasWarningsOrErrors := action . HasWarningsOrErrors ( result )
if hasWarningsOrErrors {
errorsOrWarnings ++
}
if client . Quiet && ! hasWarningsOrErrors {
continue
}
for _ , path := range paths {
result := client . Run ( [ ] string { path } , vals )
// If there is no errors/warnings and quiet flag is set
// go to the next chart
hasWarningsOrErrors := action . HasWarningsOrErrors ( result )
if hasWarningsOrErrors {
errorsOrWarnings ++
}
if client . Quiet && ! hasWarningsOrErrors {
continue
}
fmt . Fprintf ( & message , "==> Linting %s (scope: %s)\n" , single . path , scope )
fmt . Fprintf ( & message , "==> Linting %s\n" , path )
// All the Errors that are generated by a chart
// that failed a lint will be included in the
// results.Messages so we only need to print
// the Errors if there are no Messages.
if len ( result . Messages ) == 0 {
for _ , err := range result . Errors {
fmt . Fprintf ( & message , "Error %s\n" , err )
}
}
// All the Errors that are generated by a chart
// that failed a lint will be included in the
// results.Messages so we only need to print
// the Errors if there are no Messages.
if len ( result . Messages ) == 0 {
for _ , err := range result . Errors {
fmt . Fprintf ( & message , "Error %s\n" , err )
for _ , msg := range result . Messages {
if ! client . Quiet || msg . Severity > support . InfoSev {
fmt . Fprintf ( & message , "%s\n" , msg )
}
}
}
for _ , msg := range result . Messages {
if ! client . Quiet || msg . Severity > support . InfoSev {
fmt . Fprintf ( & message , "%s\n" , msg )
if len ( result . Errors ) != 0 {
failed ++
}
}
if len ( result . Errors ) != 0 {
failed ++
// Adding extra new line here to break up the
// results, stops this from being a big wall of
// text and makes it easier to follow.
fmt . Fprint ( & message , "\n" )
}
// Adding extra new line here to break up the
// results, stops this from being a big wall of
// text and makes it easier to follow.
fmt . Fprint ( & message , "\n" )
}
fmt . Fprint ( out , message . String ( ) )
summary := fmt . Sprintf ( "%d chart(s) linted, %d chart(s) failed" , len ( paths ) , failed )
summary := fmt . Sprintf ( "%d chart(s) linted, %d chart(s) failed" , linted , failed )
if failed > 0 {
return errors . New ( summary )
}
@ -153,3 +178,39 @@ func newLintCmd(out io.Writer) *cobra.Command {
return cmd
}
func getScopedValues ( parent * parentScope , single scopedChart , vals chartutil . Values ) map [ string ] chartutil . Values {
if single . root {
return map [ string ] chartutil . Values { "root" : vals }
}
result := map [ string ] chartutil . Values { }
if parent . metadata != nil {
// try to load dependent chart and match it with parent definition of dependencies
if subChart , err := loader . Load ( single . path ) ; err == nil {
for _ , dependency := range parent . metadata . Dependencies {
// it is worth remembering that there could be the same sub-chart used in many dependencies
// to handle this we should validate such sub-chart as many times (with the appropriate set of values)
// aa it is defined in the parent chart
if subChart . Metadata . Version == dependency . Version && subChart . Name ( ) == dependency . Name {
alias := dependency . Alias
if alias == "" {
alias = dependency . Name
}
tmp := chartutil . Values { }
if _ , ok := parent . values [ alias ] ; ok {
tmp = parent . values [ alias ] . ( map [ string ] interface { } )
}
if _ , ok := parent . values [ globalKey ] ; ok {
tmp [ globalKey ] = parent . values [ globalKey ] . ( map [ string ] interface { } )
}
result [ alias ] = tmp
}
}
}
}
// if we can't find any suitable values, just provide an empty set for sub-chart to at least validate other aspects
if len ( result ) == 0 {
result [ "empty" ] = chartutil . Values { }
}
return result
}