diff --git a/pkg/lintcontext/context.go b/pkg/lintcontext/context.go index ad47fb6a1..08bc56e63 100644 --- a/pkg/lintcontext/context.go +++ b/pkg/lintcontext/context.go @@ -5,6 +5,7 @@ import ( "golang.stackrox.io/kube-linter/internal/stringutils" "golang.stackrox.io/kube-linter/pkg/k8sutil" + "helm.sh/helm/v3/pkg/cli/values" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -77,7 +78,8 @@ type lintContextImpl struct { objects []Object invalidObjects []InvalidObject - customDecoder runtime.Decoder + customDecoder runtime.Decoder + helmValuesOptions values.Options } // Objects returns the (valid) objects loaded from this LintContext. @@ -106,3 +108,10 @@ func newCtx(options Options) *lintContextImpl { customDecoder: options.CustomDecoder, } } + +func newHelmCtx(options Options, helmValueOptions values.Options) *lintContextImpl { + return &lintContextImpl{ + customDecoder: options.CustomDecoder, + helmValuesOptions: helmValueOptions, + } +} diff --git a/pkg/lintcontext/create_contexts.go b/pkg/lintcontext/create_contexts.go index 5aa5b58fb..69f507737 100644 --- a/pkg/lintcontext/create_contexts.go +++ b/pkg/lintcontext/create_contexts.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "golang.stackrox.io/kube-linter/internal/set" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli/values" "k8s.io/apimachinery/pkg/runtime" ) @@ -36,6 +37,7 @@ func CreateContexts(filesOrDirs ...string) ([]LintContext, error) { // CreateContextsWithOptions creates a context with additional Options func CreateContextsWithOptions(options Options, filesOrDirs ...string) ([]LintContext, error) { contextsByDir := make(map[string]*lintContextImpl) + contextsByChartDir := make(map[string][]LintContext) for _, fileOrDir := range filesOrDirs { // Stdin if fileOrDir == "-" { @@ -59,14 +61,17 @@ func CreateContextsWithOptions(options Options, filesOrDirs ...string) ([]LintCo return nil } + if _, exists := contextsByChartDir[currentPath]; exists { + return nil + } + if !info.IsDir() { if strings.HasSuffix(strings.ToLower(currentPath), ".tgz") { - ctx := newCtx(options) - if err := ctx.loadObjectsFromTgzHelmChart(currentPath); err != nil { + lintCtxs, err := CreateHelmContextsWithOptions(HelmOptions{Options: options, FromArchive: true}, currentPath) + if err != nil { return err } - - contextsByDir[currentPath] = ctx + contextsByChartDir[currentPath] = lintCtxs return nil } @@ -85,15 +90,11 @@ func CreateContextsWithOptions(options Options, filesOrDirs ...string) ([]LintCo return nil } if isHelm, _ := chartutil.IsChartDir(currentPath); isHelm { - // Path has already been loaded, possibly through another argument. Skip. - if _, alreadyExists := contextsByDir[currentPath]; alreadyExists { - return nil - } - ctx := newCtx(options) - contextsByDir[currentPath] = ctx - if err := ctx.loadObjectsFromHelmChart(currentPath); err != nil { + lintCtxs, err := CreateHelmContextsWithOptions(HelmOptions{Options: options, FromDir: true}, currentPath) + if err != nil { return err } + contextsByChartDir[currentPath] = lintCtxs return filepath.SkipDir } return nil @@ -102,24 +103,55 @@ func CreateContextsWithOptions(options Options, filesOrDirs ...string) ([]LintCo return nil, errors.Wrapf(err, "loading from path %q", fileOrDir) } } - dirs := make([]string, 0, len(contextsByDir)) + dirs := make([]string, 0, len(contextsByDir)+len(contextsByChartDir)) for dir := range contextsByDir { dirs = append(dirs, dir) } + for dir := range contextsByChartDir { + dirs = append(dirs, dir) + } sort.Strings(dirs) var contexts []LintContext for _, dir := range dirs { + if helmCtxs, ok := contextsByChartDir[dir]; ok { + contexts = append(contexts, helmCtxs...) + continue + } contexts = append(contexts, contextsByDir[dir]) } return contexts, nil } -// CreateContextsFromHelmArchive creates a context from TGZ reader of Helm Chart. +// CreateContextsFromHelmArchive creates a context from a tgz file based on a provided tgzReader func CreateContextsFromHelmArchive(fileName string, tgzReader io.Reader) ([]LintContext, error) { - ctx := newCtx(Options{}) - if err := ctx.readObjectsFromTgzHelmChart(fileName, tgzReader); err != nil { - return nil, err - } + return CreateHelmContextsWithOptions(HelmOptions{FromReader: tgzReader}, fileName) +} + +// HelmOptions represent Helm-specific values that can be provided to modify how objects are parsed to create lint contexts +type HelmOptions struct { + Options + + // HelmValuesOptions provide options for additional values.yamls that can be provided to Helm on loading a chart + // These will be ignored for contexts that are not Helm-based + HelmValuesOptions []values.Options - return []LintContext{ctx}, nil + // Whether to treat this as a Helm chart directory + FromDir bool + // Whether to treat this as a Helm chart archive (tgz). + FromArchive bool + // FromReader is used if isDir and isArchive are both false + FromReader io.Reader +} + +// CreateContextsFromHelmArchive creates a context based on provided options +func CreateHelmContextsWithOptions(options HelmOptions, fileOrDir string) ([]LintContext, error) { + contextsByHelmValues := []LintContext{} + for _, helmValueOptions := range options.HelmValuesOptions { + ctx := newHelmCtx(options.Options, helmValueOptions) + if err := ctx.loadObjectsFromHelmChart(fileOrDir, options); err != nil { + return nil, err + } + contextsByHelmValues = append(contextsByHelmValues, ctx) + } + return contextsByHelmValues, nil } diff --git a/pkg/lintcontext/parse_helm.go b/pkg/lintcontext/parse_helm.go new file mode 100644 index 000000000..781f9ee6d --- /dev/null +++ b/pkg/lintcontext/parse_helm.go @@ -0,0 +1,82 @@ +package lintcontext + +import ( + "log" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/engine" +) + +func (l *lintContextImpl) loadObjectsFromHelmChart(path string, options HelmOptions) error { + metadata := ObjectMetadata{FilePath: path} + renderedFiles, err := l.renderHelmChart(path, options) + if err != nil { + l.addInvalidObjects(InvalidObject{Metadata: metadata, LoadErr: err}) + return nil + } + for path, contents := range renderedFiles { + // The first element of path will be the same as the last element of dir, because + // Helm duplicates it. + pathToTemplate := filepath.Join(filepath.Dir(path), path) + if err := l.loadObjectsFromReader(pathToTemplate, strings.NewReader(contents)); err != nil { + return errors.Wrapf(err, "loading objects from rendered helm chart %s/%s", path, pathToTemplate) + } + } + return nil +} + +func (l *lintContextImpl) renderHelmChart(path string, options HelmOptions) (map[string]string, error) { + // Helm doesn't have great logging behaviour, and can spam stderr, so silence their logging. + // TODO: capture these logs. + log.SetOutput(nopWriter{}) + defer log.SetOutput(os.Stderr) + + var chrt *chart.Chart + var err error + if options.FromDir && options.FromArchive { + return nil, errors.New("cannot specify that helm chart is both a directory and an archive") + } + + switch { + case options.FromArchive: + chrt, err = loader.LoadFile(path) + case options.FromDir: + chrt, err = loader.Load(path) + default: + chrt, err = loader.LoadArchive(options.FromReader) + } + if err != nil { + return nil, err + } + + if err := chrt.Validate(); err != nil { + return nil, err + } + values, err := l.helmValuesOptions.MergeValues(nil) + if err != nil { + return nil, errors.Wrap(err, "loading provided Helm value options") + } + + return l.renderValues(chrt, values) +} + +func (l *lintContextImpl) renderValues(chrt *chart.Chart, values map[string]interface{}) (map[string]string, error) { + valuesToRender, err := chartutil.ToRenderValues(chrt, values, chartutil.ReleaseOptions{Name: "test-release", Namespace: "default"}, nil) + if err != nil { + return nil, err + } + + e := engine.Engine{LintMode: true} + rendered, err := e.Render(chrt, valuesToRender) + if err != nil { + return nil, errors.Wrap(err, "failed to render") + } + + return rendered, nil +} diff --git a/pkg/lintcontext/parse_yaml.go b/pkg/lintcontext/parse_yaml.go index ae05228f9..2cbb6ab05 100644 --- a/pkg/lintcontext/parse_yaml.go +++ b/pkg/lintcontext/parse_yaml.go @@ -5,20 +5,11 @@ import ( "bytes" "fmt" "io" - "log" "os" - "path/filepath" - "strings" - y "github.com/ghodss/yaml" ocsAppsV1 "github.com/openshift/api/apps/v1" "github.com/pkg/errors" "golang.stackrox.io/kube-linter/pkg/k8sutil" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/cli/values" - "helm.sh/helm/v3/pkg/engine" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -84,102 +75,6 @@ func (w nopWriter) Write(p []byte) (n int, err error) { return len(p), nil } -func (l *lintContextImpl) renderHelmChart(dir string) (map[string]string, error) { - // Helm doesn't have great logging behaviour, and can spam stderr, so silence their logging. - // TODO: capture these logs. - log.SetOutput(nopWriter{}) - defer log.SetOutput(os.Stderr) - chrt, err := loader.Load(dir) - if err != nil { - return nil, err - } - if err := chrt.Validate(); err != nil { - return nil, err - } - valOpts := &values.Options{ValueFiles: []string{filepath.Join(dir, "values.yaml")}} - values, err := valOpts.MergeValues(nil) - if err != nil { - return nil, errors.Wrap(err, "loading values.yaml file") - } - return l.renderValues(chrt, values) -} - -func (l *lintContextImpl) renderValues(chrt *chart.Chart, values map[string]interface{}) (map[string]string, error) { - valuesToRender, err := chartutil.ToRenderValues(chrt, values, chartutil.ReleaseOptions{Name: "test-release", Namespace: "default"}, nil) - if err != nil { - return nil, err - } - - e := engine.Engine{LintMode: true} - rendered, err := e.Render(chrt, valuesToRender) - if err != nil { - return nil, errors.Wrap(err, "failed to render") - } - - return rendered, nil -} - -func (l *lintContextImpl) loadObjectsFromHelmChart(dir string) error { - metadata := ObjectMetadata{FilePath: dir} - renderedFiles, err := l.renderHelmChart(dir) - if err != nil { - l.addInvalidObjects(InvalidObject{Metadata: metadata, LoadErr: err}) - return nil - } - for path, contents := range renderedFiles { - // The first element of path will be the same as the last element of dir, because - // Helm duplicates it. - pathToTemplate := filepath.Join(filepath.Dir(dir), path) - if err := l.loadObjectsFromReader(pathToTemplate, strings.NewReader(contents)); err != nil { - return errors.Wrapf(err, "loading objects from rendered helm chart %s/%s", dir, pathToTemplate) - } - } - return nil -} - -func (l *lintContextImpl) loadObjectsFromTgzHelmChart(tgzFile string) error { - metadata := ObjectMetadata{FilePath: tgzFile} - renderedFiles, err := l.renderTgzHelmChart(tgzFile) - if err != nil { - l.invalidObjects = append(l.invalidObjects, InvalidObject{Metadata: metadata, LoadErr: err}) - return nil - } - for path, contents := range renderedFiles { - // The first element of path will be the same as the last element of tgzFile, because - // Helm duplicates it. - pathToTemplate := filepath.Join(filepath.Dir(tgzFile), path) - if err := l.loadObjectsFromReader(pathToTemplate, strings.NewReader(contents)); err != nil { - return errors.Wrapf(err, "loading objects from rendered helm chart %s/%s", tgzFile, pathToTemplate) - } - } - return nil -} - -func (l *lintContextImpl) renderTgzHelmChart(tgzFile string) (map[string]string, error) { - log.SetOutput(nopWriter{}) - defer log.SetOutput(os.Stderr) - chrt, err := loader.LoadFile(tgzFile) - - if err != nil { - return nil, err - } - if err := chrt.Validate(); err != nil { - return nil, err - } - - return l.renderChart(tgzFile, chrt) -} - -func (l *lintContextImpl) parseValues(filePath string, bytes []byte) (map[string]interface{}, error) { - currentMap := map[string]interface{}{} - - if err := y.Unmarshal(bytes, ¤tMap); err != nil { - return nil, errors.Wrapf(err, "failed to parse %s", filePath) - } - - return currentMap, nil -} - func (l *lintContextImpl) loadObjectFromYAMLReader(filePath string, r *yaml.YAMLReader) error { doc, err := r.Read() if err != nil { @@ -238,58 +133,3 @@ func (l *lintContextImpl) loadObjectsFromReader(filePath string, reader io.Reade } } } - -func (l *lintContextImpl) renderChart(fileName string, chart *chart.Chart) (map[string]string, error) { - if err := chart.Validate(); err != nil { - return nil, err - } - - valuesIndex := -1 - for i, f := range chart.Raw { - if f.Name == "values.yaml" { - valuesIndex = i - break - } - } - - indexName := filepath.Join(fileName, "values.yaml") - if valuesIndex == -1 { - return nil, errors.Errorf("%s not found", indexName) - } - - values, err := l.parseValues(indexName, chart.Raw[valuesIndex].Data) - if err != nil { - return nil, errors.Wrap(err, "loading values.yaml file") - } - - return l.renderValues(chart, values) -} - -func (l *lintContextImpl) renderTgzHelmChartReader(fileName string, tgzReader io.Reader) (map[string]string, error) { - // Helm doesn't have great logging behaviour, and can spam stderr, so silence their logging. - log.SetOutput(nopWriter{}) - defer log.SetOutput(os.Stderr) - chrt, err := loader.LoadArchive(tgzReader) - - if err != nil { - return nil, err - } - - return l.renderChart(fileName, chrt) -} - -func (l *lintContextImpl) readObjectsFromTgzHelmChart(fileName string, tgzReader io.Reader) error { - metadata := ObjectMetadata{FilePath: fileName} - renderedFiles, err := l.renderTgzHelmChartReader(fileName, tgzReader) - if err != nil { - l.invalidObjects = append(l.invalidObjects, InvalidObject{Metadata: metadata, LoadErr: err}) - return nil - } - for path, contents := range renderedFiles { - pathToTemplate := filepath.Join(fileName, path) - if err := l.loadObjectsFromReader(pathToTemplate, strings.NewReader(contents)); err != nil { - return errors.Wrapf(err, "loading objects from rendered helm chart %s", pathToTemplate) - } - } - return nil -}