Skip to content

Commit

Permalink
Merge pull request #6 from dadav/embed_schema
Browse files Browse the repository at this point in the history
Embed schema
  • Loading branch information
dadav authored Aug 20, 2023
2 parents 460b44c + 5827810 commit e4268f9
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 39 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Flags:
-l, --log-level string Level of logs that should printed, one of (panic, fatal, error, warning, info, debug, trace) (default "info")
-n, --no-dependencies don't analyze dependencies
-o, --output-file string jsonschema file path relative to each chart directory to which jsonschema will be written (default "values.schema.json")
-r, --use-refereces use references instead of embeding dependencies schema
-f, --value-files strings filenames to check for chart values (default [values.yaml])
-v, --version version for helm-schema
```
Expand Down
14 changes: 9 additions & 5 deletions cmd/helm-schema/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ func configureLogging() {
log.SetLevel(logLevel)
}

func newCommand(run func(cmd *cobra.Command, args []string)) (*cobra.Command, error) {
func newCommand(run func(cmd *cobra.Command, args []string) error) (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "helm-schema",
Short: "helm-schema automatically generates a jsonschema file for helm charts from values files",
Version: version,
Run: run,
Use: "helm-schema",
Short: "helm-schema automatically generates a jsonschema file for helm charts from values files",
Version: version,
RunE: run,
SilenceUsage: true,
SilenceErrors: true,
}

logLevelUsage := fmt.Sprintf(
Expand All @@ -48,6 +50,8 @@ func newCommand(run func(cmd *cobra.Command, args []string)) (*cobra.Command, er
StringP("chart-search-root", "c", ".", "directory to search recursively within for charts")
cmd.PersistentFlags().
BoolP("dry-run", "d", false, "don't actually create files just print to stdout passed")
cmd.PersistentFlags().
BoolP("use-refereces", "r", false, "use references instead of embeding dependencies schema")
cmd.PersistentFlags().
BoolP("keep-full-comment", "s", false, "If this flag is used, comment won't be cut off if two newlines are found.")
cmd.PersistentFlags().
Expand Down
183 changes: 149 additions & 34 deletions cmd/helm-schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"sync"

"github.com/dadav/helm-schema/pkg/chart"
Expand Down Expand Up @@ -38,35 +39,50 @@ func searchFiles(startPath, fileName string, queue chan<- string, errs chan<- er
}
}

type Result struct {
ChartPath string
ValuesPath string
Chart *chart.ChartFile
Schema map[string]interface{}
Errors []error
}

func worker(
dryRun, skipDeps, keepFullComment bool,
dryRun, skipDeps, useRef, keepFullComment bool,
valueFileNames []string,
outFile string,
queue <-chan string,
errs chan<- error,
results chan<- Result,
) {
for chartPath := range queue {
result := Result{ChartPath: chartPath}

chartBasePath := filepath.Dir(chartPath)
file, err := os.Open(chartPath)
if err != nil {
errs <- err
result.Errors = append(result.Errors, err)
results <- result
continue
}

chart, err := chart.ReadChart(file)
if err != nil {
errs <- err
result.Errors = append(result.Errors, err)
results <- result
continue
}
result.Chart = &chart

var valuesPath string
var valuesFound bool
errorsWeMaybeCanIgnore := []error{}

for _, possibleValueFileName := range valueFileNames {
valuesPath = filepath.Join(chartBasePath, possibleValueFileName)
_, err := os.Stat(valuesPath)
if err != nil {
if !errors.Is(os.ErrNotExist, err) {
errs <- err
if !os.IsNotExist(err) {
errorsWeMaybeCanIgnore = append(errorsWeMaybeCanIgnore, err)
}
continue
}
Expand All @@ -75,72 +91,73 @@ func worker(
}

if !valuesFound {
for _, err := range errorsWeMaybeCanIgnore {
result.Errors = append(result.Errors, err)
}
result.Errors = append(result.Errors, errors.New("No values file found."))
results <- result
continue
}
result.ValuesPath = valuesPath

valuesFile, err := os.Open(valuesPath)
if err != nil {
errs <- err
result.Errors = append(result.Errors, err)
results <- result
continue
}
content, err := util.ReadFileAndFixNewline(valuesFile)
if err != nil {
errs <- err
result.Errors = append(result.Errors, err)
results <- result
continue
}

var values yaml.Node
err = yaml.Unmarshal(content, &values)
if err != nil {
log.Fatal(err)
result.Errors = append(result.Errors, err)
results <- result
continue
}

schema := schema.YamlToJsonSchema(&values, keepFullComment, nil)
mainSchema := schema.YamlToJsonSchema(&values, keepFullComment, nil)
result.Schema = mainSchema

if !skipDeps {
for _, dep := range chart.Dependencies {
if depName, ok := dep["name"].(string); ok {
schema["properties"].(map[string]interface{})[depName] = map[string]string{
"title": chart.Name,
"description": chart.Description,
"$ref": fmt.Sprintf("charts/%s/%s", depName, outFile),
if useRef {
mainSchema["properties"].(map[string]interface{})[depName] = map[string]string{
"title": chart.Name,
"description": chart.Description,
"$ref": fmt.Sprintf("charts/%s/%s", depName, outFile),
}
}
}
}
}

jsonStr, err := json.MarshalIndent(schema, "", " ")

if dryRun {
log.Infof("Printing jsonschema for %s chart", chart.Name)
fmt.Printf("%s\n", jsonStr)
} else {
if err := os.WriteFile(filepath.Join(chartBasePath, outFile), jsonStr, 0644); err != nil {
errs <- err
continue
}
}

results <- result
}
}

func exec(_ *cobra.Command, _ []string) {
func exec(cmd *cobra.Command, _ []string) error {
configureLogging()

chartSearchRoot := viper.GetString("chart-search-root")
dryRun := viper.GetBool("dry-run")
useRef := viper.GetBool("use-references")
noDeps := viper.GetBool("no-dependencies")
keepFullComment := viper.GetBool("keep-full-comment")
outFile := viper.GetString("output-file")
valueFileNames := viper.GetStringSlice("value-files")

workersCount := 1
if !dryRun {
workersCount = runtime.NumCPU()
}
workersCount := runtime.NumCPU() * 2

// 1. Start a producer that searches Chart.yaml and values.yaml files
queue := make(chan string)
resultsChan := make(chan Result)
results := []Result{}
errs := make(chan error)
done := make(chan struct{})

Expand All @@ -158,7 +175,16 @@ func exec(_ *cobra.Command, _ []string) {

go func() {
defer wg.Done()
worker(dryRun, noDeps, keepFullComment, valueFileNames, outFile, queue, errs)
worker(
dryRun,
noDeps,
useRef,
keepFullComment,
valueFileNames,
outFile,
queue,
resultsChan,
)
}()
}

Expand All @@ -167,12 +193,102 @@ loop:
select {
case err := <-errs:
log.Error(err)
case res := <-resultsChan:
results = append(results, res)
case <-done:
break loop

}
}

// Sort results if dependencies should be processed
// Need to resolve the dependencies from deepest level to highest
if !noDeps && !useRef {
sort.Slice(results, func(i, j int) bool {
first := results[i]
second := results[j]

// No dependencies
if len(first.Chart.Dependencies) == 0 {
return true
}
// First is dependency of second
for _, dep := range second.Chart.Dependencies {
if name, ok := dep["name"]; ok {
if name == first.Chart.Name {
return true
}
}
}

// first comes after second
return false
})
}

chartNameToResult := make(map[string]Result)
foundErrors := false

// process results
for _, result := range results {
// Error handling
if len(result.Errors) > 0 {
foundErrors = true
if result.Chart != nil {
log.Errorf(
"Found %d errors while processing the chart %s (%s)",
len(result.Errors),
result.Chart.Name,
result.ChartPath,
)
} else {
log.Errorf("Found %d errors while processing the chart %s", len(result.Errors), result.ChartPath)
}
for _, err := range result.Errors {
log.Error(err)
}
continue
}

// Embed dependencies if needed
if !noDeps && !useRef {
for _, dep := range result.Chart.Dependencies {
if depName, ok := dep["name"].(string); ok {
if dependencyResult, ok := chartNameToResult[depName]; ok {
result.Schema["properties"].(map[string]interface{})[depName] = map[string]interface{}{
"type": "object",
"title": depName,
"description": dependencyResult.Chart.Description,
"properties": dependencyResult.Schema["properties"],
}
}
}
}
chartNameToResult[result.Chart.Name] = result
}

// Print to stdout or write to file
jsonStr, err := json.MarshalIndent(result.Schema, "", " ")
if err != nil {
log.Error(err)
continue
}

if dryRun {
log.Infof("Printing jsonschema for %s chart (%s)", result.Chart.Name, result.ChartPath)
fmt.Printf("%s\n", jsonStr)
} else {
chartBasePath := filepath.Dir(result.ChartPath)
if err := os.WriteFile(filepath.Join(chartBasePath, outFile), jsonStr, 0644); err != nil {
errs <- err
continue
}
}
}
if foundErrors {
return errors.New("foo")
}
return nil
}

func main() {
Expand All @@ -183,7 +299,6 @@ func main() {
}

if err := command.Execute(); err != nil {
log.Errorf("Failed to start the CLI: %s", err)
os.Exit(1)
}
}
10 changes: 10 additions & 0 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,16 @@ func YamlToJsonSchema(
keepFullComment,
&requiredProperties,
)

if _, ok := schema["properties"].(map[string]interface{})["global"]; !ok {
// global key must be present, otherwise helm lint will fail
schema["properties"].(map[string]interface{})["global"] = map[string]interface{}{
"type": "object",
"title": "global",
"description": "Global values are values that can be accessed from any chart or subchart by exactly the same name.",
}
}

if len(requiredProperties) > 0 {
schema["required"] = requiredProperties
}
Expand Down

0 comments on commit e4268f9

Please sign in to comment.