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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,40 @@ Flags:
--use-default uses default template unless a custom template is present (default true)
```

## Values Modeline

The modeline subcommand is useful for setting a chart's schema in the yaml modeline of a values file.

For instance, running the following:

```
helm values modeline brahmlower-kiwix/kiwix ./kiwix-values.yaml --version 0.1.1
```

results in the following line being added to the top of the document:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/brahmlower/helm-kiwix/refs/tags/kiwix-0.1.1/charts/kiwix/values.schema.json

...
```

Options:

```
Add yaml-language-server modeline to values file

Usage:
helm-values modeline [flags] chart_ref [values_file]

Flags:
-f, --force replace existing modeline
-h, --help help for modeline
--log-level string log level (debug, info, warn, error, fatal, panic) (default "warn")
-p, --parents create parent directories if they don't exist
--version string chart version (for remote charts)
```

## Schema Comments

This plugin simplifies schema markup in the values.yaml comments.
Expand Down Expand Up @@ -419,6 +453,7 @@ and [helm-docs](https://github.com/norwoodj/helm-docs).
- [x] Built-in plugin update mechanism
- 0.3.0
- [x] Pre-Commit Hook support
- [x] Values modeline support
- [ ] Schema Generation
- [ ] Warn on ignored jsonschema property (in cases of $ref/$schema usage)
- [ ] Json-Schema Draft 7 support?
Expand Down
57 changes: 57 additions & 0 deletions cmd/helm-values/internal/config/modeline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package config

import (
"helmvalues/pkg/modeline"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func NewModelineConfig() *ModelineConfig {
cfg := standardViper()

return &ModelineConfig{cfg}
}

type ModelineConfig struct {
*viper.Viper
}

func (c *ModelineConfig) LogLevel() (logrus.Level, error) {
return logrus.ParseLevel(c.GetString("log-level"))
}

func (c *ModelineConfig) UpdateLogger(logger *logrus.Logger) error {
level, err := c.LogLevel()
if err != nil {
return err
}

logger.SetLevel(level)
return nil
}

func (c *ModelineConfig) BindFlags(cmd *cobra.Command) {
cmd.Flags().BoolP("parents", "p", false, "create parent directories if they don't exist")
c.BindPFlag("parents", cmd.Flags().Lookup("parents"))
c.BindEnv("parents")

cmd.Flags().String("version", "", "chart version (for remote charts)")
c.BindPFlag("version", cmd.Flags().Lookup("version"))
c.BindEnv("version")

cmd.Flags().String("log-level", "warn", "log level (debug, info, warn, error, fatal, panic)")
c.BindPFlag("log-level", cmd.Flags().Lookup("log-level"))
c.BindEnv("log-level")
}

func (c *ModelineConfig) ToPackageConfig(chartRef string, targetFile string) *modeline.Config {
return &modeline.Config{
ChartRef: chartRef,
ChartVersion: c.GetString("version"),
TargetFile: targetFile,
CreateParents: c.GetBool("parents"),
PartialModeline: modeline.NewPartialModeline("yaml-language-server", "$schema"),
}
}
31 changes: 31 additions & 0 deletions cmd/helm-values/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"helmvalues/cmd/helm-values/internal"
"helmvalues/cmd/helm-values/internal/config"
"helmvalues/pkg/docs"
"helmvalues/pkg/modeline"
"helmvalues/pkg/schema"

"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -46,6 +47,7 @@ func Program(logger *logrus.Logger) *cobra.Command {
cmd.AddCommand(CommandSchema(logger, generationGroup))
cmd.AddCommand(CommandDocs(logger, generationGroup))
cmd.AddCommand(CommandPreCommit(logger))
cmd.AddCommand(CommandModeline(logger, generationGroup))
cmd.AddCommand(CommandUpdate(logger))
cmd.AddCommand(CommandVersion(logger))
return cmd
Expand Down Expand Up @@ -102,6 +104,35 @@ func CommandDocs(logger *logrus.Logger, group *cobra.Group) *cobra.Command {
return cmd
}

func CommandModeline(logger *logrus.Logger, group *cobra.Group) *cobra.Command {
cfg := config.NewModelineConfig()

cmd := &cobra.Command{
Use: "modeline [flags] chart_ref [values_file]",
Short: "Add yaml-language-server modeline to values file",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
if err := cfg.UpdateLogger(logger); err != nil {
return err
}

chartRef := args[0]
valuesFile := ""
if len(args) > 1 {
valuesFile = args[1]
}

modelineCfg := cfg.ToPackageConfig(chartRef, valuesFile)
return modeline.WriteModeline(logger, modelineCfg)
},
GroupID: group.ID,
}

cfg.BindFlags(cmd)

return cmd
}

func CommandUpdate(logger *logrus.Logger) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module helmvalues
go 1.24.2

require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/samber/lo v1.52.0
Expand All @@ -17,7 +18,6 @@ require (
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
Expand Down
15 changes: 13 additions & 2 deletions pkg/charts/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ func (p *Chart) ReadmeRstTemplateFilePath() string {
}

type ChartDetails struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Version string `yaml:"version"`
Annotations map[string]string `yaml:"annotations"`
}

func (d *ChartDetails) ValuesSchema() string {
schemaURL, ok := d.Annotations["values-schema"]
if !ok || schemaURL == "" {
return ""
}

return schemaURL
}
69 changes: 69 additions & 0 deletions pkg/helm/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package helm

import (
"fmt"
"helmvalues/pkg/charts"
"os"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
)

func GetCacheHome() (string, error) {
cacheHome := os.Getenv("HELM_CACHE_HOME")
if cacheHome == "" {
return "", fmt.Errorf("HELM_CACHE_HOME environment variable is not set")
}
return cacheHome, nil
}

func FindRepositoryIndex(logger *logrus.Logger, repoName string) (*Index, error) {
cacheHome, err := GetCacheHome()
if err != nil {
return nil, err
}

indexPath := filepath.Join(cacheHome, "repository", repoName+"-index.yaml")

if _, err := os.Stat(indexPath); err != nil {
return nil, err
}

return LoadIndex(indexPath)
}

// GetChartFromCache retrieves chart information from the Helm cache
func GetChartFromCache(logger *logrus.Logger, chartRef string, version string) (*charts.ChartDetails, error) {
// Parse chart reference
parts := strings.SplitN(chartRef, "/", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid chart reference: %s", chartRef)
}
repoName := parts[0]
chartName := parts[1]

// Find the repository index
index, err := FindRepositoryIndex(logger, repoName)
if err != nil {
return nil, err
}

// Get the specific version or latest
var chartVersion *charts.ChartDetails
if version != "" {
chartVersion, err = index.GetVersion(chartName, version)
if err != nil {
return nil, err
}
logger.Infof("Using chart %s version %s", chartName, version)
} else {
chartVersion, err = index.GetLatestVersion(chartName)
if err != nil {
return nil, err
}
logger.Infof("Using latest stable version of %s: %s", chartName, chartVersion.Version)
}

return chartVersion, nil
}
33 changes: 33 additions & 0 deletions pkg/helm/chart_details.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package helm

import (
"fmt"
"helmvalues/pkg/charts"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
)

func ChartDetailsFromRef(chartRef string, version string) (*charts.ChartDetails, error) {
refParts := strings.Split(chartRef, "/")
if len(refParts) == 2 {
return GetChartFromCache(logrus.StandardLogger(), chartRef, version)
}

return chartDetailsFromPath(chartRef)
}

func chartDetailsFromPath(chartRef string) (*charts.ChartDetails, error) {
absPath, err := filepath.Abs(chartRef)
if err != nil {
return nil, fmt.Errorf("failed to resolve chart path: %w", err)
}

chart, err := charts.NewChart(absPath)
if err != nil {
return nil, fmt.Errorf("failed to load chart: %w", err)
}

return chart.Details, nil
}
100 changes: 100 additions & 0 deletions pkg/helm/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package helm

import (
"fmt"
"helmvalues/pkg/charts"
"os"
"sort"

"github.com/Masterminds/semver/v3"
"go.yaml.in/yaml/v4"
)

type Index struct {
Entries map[string][]*charts.ChartDetails `yaml:"entries"`
}

// LoadIndex loads and parses a Helm index.yaml file
func LoadIndex(path string) (*Index, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read index file: %w", err)
}

var index Index
if err := yaml.Unmarshal(data, &index); err != nil {
return nil, fmt.Errorf("failed to parse index file: %w", err)
}

return &index, nil
}

// FindChart finds chart versions by name in the index
func (i *Index) FindChart(chartName string) ([]*charts.ChartDetails, error) {
versions, ok := i.Entries[chartName]
if !ok {
return nil, fmt.Errorf("chart %q not found in index", chartName)
}

if len(versions) == 0 {
return nil, fmt.Errorf("no versions found for chart %q", chartName)
}

return versions, nil
}

// GetVersion finds a specific version of a chart
func (i *Index) GetVersion(chartName, version string) (*charts.ChartDetails, error) {
versions, err := i.FindChart(chartName)
if err != nil {
return nil, err
}

for _, v := range versions {
if v.Version == version {
return v, nil
}
}

return nil, fmt.Errorf("version %q not found for chart %q", version, chartName)
}

// GetLatestVersion finds the latest stable version of a chart
// Stable means non-prerelease versions (no -alpha, -beta, -rc suffixes)
func (i *Index) GetLatestVersion(chartName string) (*charts.ChartDetails, error) {
versions, err := i.FindChart(chartName)
if err != nil {
return nil, err
}

// Parse and sort versions
semvers := []*semver.Version{}
versionMap := map[string]*charts.ChartDetails{}

for _, v := range versions {
sv, err := semver.NewVersion(v.Version)
if err != nil {
// Skip invalid semver versions
continue
}
semvers = append(semvers, sv)
versionMap[v.Version] = v
}

if len(semvers) == 0 {
return nil, fmt.Errorf("no valid semver versions found for chart %q", chartName)
}

// Sort in descending order
sort.Sort(sort.Reverse(semver.Collection(semvers)))

// Find the first stable (non-prerelease) version
for _, sv := range semvers {
if sv.Prerelease() == "" {
return versionMap[sv.Original()], nil
}
}

// If no stable version found, return the latest version regardless
return versionMap[semvers[0].Original()], nil
}
Loading