Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ Usage:
helm-values schema [flags] chart_dir [...chart_dir]

Flags:
--check check that values.schema.json is up to date and that Chart.yaml's values-schema annotation references the chart's current version, without writing changes (exit non-zero if not)
--dry-run don't write changes to disk
--git-add stage changes with git add (useful for pre-commit hooks)
-h, --help help for schema
--log-level string log level (debug, info, warn, error, fatal, panic) (default "warn")
--stdout write to stdout
Expand Down Expand Up @@ -121,11 +123,14 @@ Usage:
helm-values docs [flags] chart_dir [...chart_dir]

Flags:
--check check that the rendered docs file is up to date, without writing changes (exit non-zero if not)
--dry-run don't write changes to disk
--extra-templates string glob path to extra templates
--git-add stage changes with git add (useful for pre-commit hooks)
-h, --help help for docs
--log-level string log level (debug, info, warn, error, fatal, panic) (default "warn")
--markup string markup language (md, markdown, rst, restructuredtext)
--order string order of values (preserve, alphabetical) (default "preserve")
--output string path to output (defaults to README.md or README.rst based on markup)
--stdout write to stdout
--strict fail on doc comment parsing errors
Expand Down Expand Up @@ -471,6 +476,8 @@ and [helm-docs](https://github.com/norwoodj/helm-docs).
- [x] Pre-Commit Hook support
- [x] Values modeline support
- 0.4.0
- [x] `schema`/`docs` `--check` flag for CI validation
- 0.5.0
- [ ] Schema Generation
- [ ] Warn on ignored jsonschema property (in cases of $ref/$schema usage)
- [ ] Json-Schema Draft 7 support?
Expand All @@ -482,14 +489,14 @@ and [helm-docs](https://github.com/norwoodj/helm-docs).
- [ ] Support "Deprecated" indicator
- [ ] Template: Chart Values
- [x] Values groups
- 0.5.0
- 0.6.0
- [ ] Schema Generation
- [ ] Support declaring and using yaml anchors in doc comments
- [ ] Root level one-of/any-of/all-of
- [ ] Docs Generation
- [ ] TODO: Detect recursive templates
- [ ] TODO: markdown/rst escaping
- 0.6.0
- 0.7.0
- [ ] Template: Chart Dependencies (defined in Chart.yaml)
- some day
- [ ] validate examples against schema
Expand Down
6 changes: 5 additions & 1 deletion cmd/helm-values/internal/config/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,13 @@ func (c *DocsConfig) BindFlags(cmd *cobra.Command) error {
cmd.Flags().String("output", "", "path to output (defaults to README.md or README.rst based on markup)")
cmd.Flags().String("template", "", "path to template (defaults to README.md.tmpl or README.rst.tmpl based on markup)")
cmd.Flags().String("extra-templates", "", "glob path to extra templates")
cmd.Flags().Bool("check", false,
"check that the rendered docs file is up to date, without writing "+
"changes (exit non-zero if not)")

for _, name := range []string{
"stdout", "git-add", "strict", "dry-run", logLevelFlag, "markup",
"order", "use-default", "output", "template", "extra-templates",
"order", "use-default", "output", "template", "extra-templates", "check",
} {
if err := bindFlag(c.Viper, cmd, name); err != nil {
return err
Expand Down Expand Up @@ -166,6 +169,7 @@ func (c *DocsConfig) ToPackageConfig() (*docs.Config, error) {
Strict: c.GetBool("strict"),
DryRun: c.GetBool("dry-run"),
GitAdd: c.GetBool("git-add"),
Check: c.GetBool("check"),
UseDefault: c.UseDefault(),
Output: c.Output(),
Template: c.GetString("template"),
Expand Down
7 changes: 6 additions & 1 deletion cmd/helm-values/internal/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ func (c *SchemaConfig) BindFlags(cmd *cobra.Command) error {
cmd.Flags().Bool("dry-run", false, "don't write changes to disk")
cmd.Flags().String(logLevelFlag, "warn", "log level (debug, info, warn, error, fatal, panic)")
cmd.Flags().Bool("write-modeline", true, "write modeline to values file")
cmd.Flags().Bool("check", false,
"check that values.schema.json is up to date and that Chart.yaml's "+
"values-schema annotation references the chart's current version, "+
"without writing changes (exit non-zero if not)")

for _, name := range []string{
"stdout", "strict", "git-add", "dry-run", logLevelFlag, "write-modeline",
"stdout", "strict", "git-add", "dry-run", logLevelFlag, "write-modeline", "check",
} {
if err := bindFlag(c.Viper, cmd, name); err != nil {
return err
Expand All @@ -79,6 +83,7 @@ func (c *SchemaConfig) ToPackageConfig() (*schema.Config, error) {
DryRun: c.GetBool("dry-run"),
GitAdd: c.GetBool("git-add"),
WriteModeline: c.GetBool("write-modeline"),
Check: c.GetBool("check"),
LogLevel: logLevel,
}

Expand Down
1 change: 1 addition & 0 deletions pkg/docs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Config struct {
Strict bool
DryRun bool
GitAdd bool
Check bool
UseDefault mo.Option[bool]
Output mo.Option[string]
Template string
Expand Down
47 changes: 35 additions & 12 deletions pkg/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,32 @@ func GenerateDocs(logger *logrus.Logger, cfg *Config, chartDirs []string) error
}

// Iterate through plans, generating the docs for each
var problems []string

for _, plan := range plans {
if err := generateChartDoc(logger, cfg, staticPaths, plan); err != nil {
chartProblems, err := generateChartDoc(logger, cfg, staticPaths, plan)
if err != nil {
return err
}

problems = append(problems, chartProblems...)
}

return reportProblems(problems)
}

// reportProblems prints every check problem found and, if any were found,
// returns an error summarizing how many.
func reportProblems(problems []string) error {
if len(problems) == 0 {
return nil
}

for _, p := range problems {
fmt.Fprintln(os.Stderr, p)
}

return nil
return fmt.Errorf("check failed: %d problem(s) found", len(problems))
}

// collectPlans builds a Plan for each discovered chart, logging its details and
Expand All @@ -69,18 +88,18 @@ func collectPlans(logger *logrus.Logger, cfg *Config, chartsFound []*charts.Char
return plans, nil
}

// generateChartDoc renders and writes the documentation for a single chart's plan,
// using staticPaths and cfg.ExtraTemplates as additional templates.
func generateChartDoc(logger *logrus.Logger, cfg *Config, staticPaths []string, plan *Plan) error {
// generateChartDoc renders the documentation for a single chart's plan, using staticPaths
// and cfg.ExtraTemplates as additional templates, then either writes or checks it
// depending on the plan's configuration. The returned problems are non-empty only when
// plan.Check() is set and the rendered content doesn't match what's on disk.
func generateChartDoc(logger *logrus.Logger, cfg *Config, staticPaths []string, plan *Plan) ([]string, error) {
logger.Infof("docs: %s: starting generation", plan.Chart().Details.Name)

logger.Debugf("docs: %s: reading values file", plan.Chart().Details.Name)

jsonschema, err := schema.NewGenerator(logger, plan.SchemaPlan()).Generate()
if err != nil {
logger.Error(err.Error())

return nil
return nil, fmt.Errorf("docs: %s: %w", plan.Chart().Details.Name, err)
}

logger.Tracef("docs: %s: jsonschema properties: %+v", plan.Chart().Details.Name, jsonschema.Properties)
Expand All @@ -95,7 +114,7 @@ func generateChartDoc(logger *logrus.Logger, cfg *Config, staticPaths []string,

t, err := buildChartTemplate(logger, cfg, staticPaths, plan)
if err != nil {
return err
return nil, err
}

buf := new(bytes.Buffer)
Expand All @@ -104,18 +123,22 @@ func generateChartDoc(logger *logrus.Logger, cfg *Config, staticPaths []string,

err = t.Execute(buf, table)
if err != nil {
return fmt.Errorf("rendering template: %w", err)
return nil, fmt.Errorf("rendering template: %w", err)
}

if plan.Check() {
return plan.CheckReadme(buf.String())
}

logger.Debugf("docs: %s: writing output", plan.Chart().Details.Name)

if err := plan.WriteReadme(logger, buf.String()); err != nil {
return err
return nil, err
}

logger.Infof("docs: %s: finished", plan.Chart().Details.Name)

return nil
return nil, nil
}

// buildChartTemplate collects the static, extra, and (if applicable) custom template
Expand Down
32 changes: 32 additions & 0 deletions pkg/docs/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func NewPlan(cfg *Config, chart *charts.Chart) *Plan {
DryRun: cfg.DryRun,
GitAdd: cfg.GitAdd,
WriteModeline: false,
Check: false,
LogLevel: cfg.LogLevel,
}
schemaPlan := schema.NewPlan(schemaCfg, chart)
Expand Down Expand Up @@ -103,6 +104,37 @@ func (p *Plan) DryRun() bool {
return p.cfg.DryRun
}

// Check reports whether the plan should verify (rather than write) the
// chart's rendered documentation.
func (p *Plan) Check() bool {
return p.cfg.Check
}

// CheckReadme reports whether the plan's rendered content matches what's
// currently on disk. An empty, nil-error result means it's up to date.
func (p *Plan) CheckReadme(content string) ([]string, error) {
outputPath, err := p.DocsOutputPath()
if err != nil {
return nil, err
}

// outputPath is derived from the chart's own configured/derived README path
// (or a user-supplied --output flag), not attacker-controlled input.
//nolint:gosec // outputPath is a user-opted-in, non-attacker-controlled file path
existing, err := os.ReadFile(outputPath)
// A missing or unreadable output file is itself a check problem (stale/missing docs),
// not a fatal error, so it's intentionally not propagated as one.
//nolint:nilerr // see comment above
if err != nil || string(existing) != content {
return []string{fmt.Sprintf(
"%s is stale -- run `helm-values docs %s` and commit the result",
outputPath, p.chart.RootPath(),
)}, nil
}

return nil, nil
}

// DocsTargetTemplate resolves the template path to render, returning whether the
// built-in default template should be used, or an error if no template can be found.
func (p *Plan) DocsTargetTemplate() (string, bool, error) {
Expand Down
72 changes: 72 additions & 0 deletions pkg/docs/plan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package docs_test

import (
"os"
"path/filepath"
"testing"

"helmvalues/pkg/charts"
"helmvalues/pkg/docs"
"helmvalues/pkg/docs/templates"

"github.com/samber/mo"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func strPtr(s string) *string { return &s }

func TestPlanCheckReadme(t *testing.T) {
t.Parallel()

const rendered = "# test-chart\n\nrendered content\n"

tests := []struct {
name string
existingFile *string
wantProblems int
}{
{name: "readme matches", existingFile: strPtr(rendered), wantProblems: 0},
{name: "readme stale", existingFile: strPtr("old content\n"), wantProblems: 1},
{name: "readme missing", existingFile: nil, wantProblems: 1},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

dir := t.TempDir()
chartYaml := "name: test-chart\ndescription: a test chart\nversion: 1.0.0\n"
require.NoError(t, os.WriteFile(filepath.Join(dir, "Chart.yaml"), []byte(chartYaml), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "values.yaml"), []byte("foo: bar\n"), 0o600))

chart, err := charts.NewChart(dir)
require.NoError(t, err)

if tc.existingFile != nil {
require.NoError(t, os.WriteFile(chart.ReadmeMdFilePath(), []byte(*tc.existingFile), 0o600))
}

cfg := &docs.Config{
LogLevel: logrus.WarnLevel,
StdOut: false,
Strict: false,
DryRun: false,
GitAdd: false,
Check: true,
UseDefault: mo.None[bool](),
Output: mo.None[string](),
Template: "",
ExtraTemplates: nil,
Markup: mo.None[templates.Markup](),
Order: docs.ValuesOrderPreserve,
}
plan := docs.NewPlan(cfg, chart)

problems, err := plan.CheckReadme(rendered)
require.NoError(t, err)
assert.Len(t, problems, tc.wantProblems)
})
}
}
1 change: 1 addition & 0 deletions pkg/schema/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ type Config struct {
DryRun bool
GitAdd bool
WriteModeline bool
Check bool
LogLevel logrus.Level
}
41 changes: 41 additions & 0 deletions pkg/schema/plan.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package schema

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

"helmvalues/pkg"
"helmvalues/pkg/charts"
Expand Down Expand Up @@ -75,6 +77,45 @@ func (p *Plan) DryRun() bool {
return p.cfg.DryRun
}

// Check reports whether the plan should verify (rather than write) the
// chart's generated schema and Chart.yaml metadata.
func (p *Plan) Check() bool {
return p.cfg.Check
}

// CheckSchema reports problems with the chart's on-disk schema and
// Chart.yaml metadata: whether values.schema.json matches the freshly
// generated schema, and whether the values-schema annotation references
// the chart's current version. An empty result means everything is up to
// date.
func (p *Plan) CheckSchema(schema *pkg.JsonSchema) []string {
var problems []string

content, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return []string{fmt.Sprintf("%s: failed to marshal generated schema: %s", p.chart.Details.Name, err)}
}

existing, err := os.ReadFile(p.chart.SchemaFilePath())
if err != nil || !bytes.Equal(existing, content) {
problems = append(problems, fmt.Sprintf(
"%s is stale -- run `helm-values schema %s` and commit the result",
p.chart.SchemaFilePath(), p.chart.RootPath(),
))
}

if schemaURL := p.chart.Details.ValuesSchema(); schemaURL != "" {
if !strings.Contains(schemaURL, p.chart.Details.Version) {
problems = append(problems, fmt.Sprintf(
"%s: values-schema annotation does not reference current chart version %s (found: %s)",
p.chart.ChartFilePath(), p.chart.Details.Version, schemaURL,
))
}
}

return problems
}

// WriteSchema encodes and writes the generated schema per the plan's config.
func (p *Plan) WriteSchema(_ *logrus.Logger, schema *pkg.JsonSchema) error {
content, err := json.MarshalIndent(schema, "", " ")
Expand Down
Loading