Skip to content

Commit

Permalink
Merge pull request #77 from losisin/feature/add-config-file
Browse files Browse the repository at this point in the history
feature: read configuration from file
  • Loading branch information
losisin committed Jun 24, 2024
2 parents 8d799bf + a2adf79 commit 3682786
Show file tree
Hide file tree
Showing 14 changed files with 690 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ changelog:
order: 999
filters:
exclude:
- "^Merge pull request #" # exclude merge commits
- "^Merge" # exclude merge commits
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/losisin/helm-values-schema-json
rev: v1.4.1
rev: v1.5.0
hooks:
- id: helm-schema
args:
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ First [install pre-commit](https://pre-commit.com/#install) and then create or u
```yaml
repos:
- repo: https://github.com/losisin/helm-values-schema-json
rev: v1.4.1
rev: v1.5.0
hooks:
- id: helm-schema
args: ["-input", "values.yaml"]
Expand Down Expand Up @@ -89,7 +89,7 @@ This is a great tool for adding git hooks to your project. You can find it's doc

```bash
$ helm schema -help
usage: helm schema [-input STR] [-draft INT] [-output STR]
Usage: helm schema [options...] <arguments>
-draft int
Draft version (4, 6, 7, 2019, or 2020) (default 2020)
-indent int
Expand All @@ -108,7 +108,35 @@ usage: helm schema [-input STR] [-draft INT] [-output STR]
JSON schema title
```

### Basic
### Configuration file

This plugin will look for it's configuration file called `schema.yaml` in the current working directory. All options available from CLI can be set in this file. Example:

```yaml
# Required
input:
- schema.yaml

draft: 2020
indent: 4
output: values.schema.json

schemaRoot:
id: https://example.com/schema
title: Helm Values Schema
description: Schema for Helm values
additionalProperties: true
```

Then, just run the plugin without any arguments:

```bash
$ helm schema
```

### CLI

#### Basic

In most cases you will want to run the plugin with default options:

Expand All @@ -118,9 +146,9 @@ $ helm schema -input values.yaml

This will read `values.yaml`, set draft version to `2020-12` and save outpout to `values.schema.json`.

### Extended
#### Extended

#### Multiple values files
##### Multiple values files

Merge multiple values files, set json-schema draft version explicitly and save output to `my.schema.json`:

Expand Down Expand Up @@ -217,7 +245,7 @@ Output will be something like this:
}
```

#### Root JSON object properties
##### Root JSON object properties

Adding ID, title and description to the schema:

Expand Down
21 changes: 18 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,31 @@ import (
)

func main() {
conf, output, err := pkg.ParseFlags(os.Args[0], os.Args[1:])
// Load configuration from a YAML file
fileConfig, err := pkg.LoadConfig("schema.yaml")
if err != nil {
fmt.Println("Error loading config file:", err)
}

// Parse CLI flags
flagConfig, output, err := pkg.ParseFlags(os.Args[0], os.Args[1:])
if err == flag.ErrHelp {
fmt.Println(output)
return
} else if err != nil {
fmt.Println("Error:", output)
fmt.Println("Error parsing flags:", output)
return
}

err = pkg.GenerateJsonSchema(conf)
// Merge configurations, giving precedence to CLI flags
var finalConfig *pkg.Config
if fileConfig != nil {
finalConfig = pkg.MergeConfig(fileConfig, flagConfig)
} else {
finalConfig = flagConfig
}

err = pkg.GenerateJsonSchema(finalConfig)
if err != nil {
fmt.Println("Error:", err)
}
Expand Down
45 changes: 41 additions & 4 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bytes"
"io"
"log"
"os"
"testing"

Expand All @@ -13,6 +14,8 @@ func TestMain(t *testing.T) {
tests := []struct {
name string
args []string
setup func()
cleanup func()
expectedError string
expectedOut string
}{
Expand Down Expand Up @@ -40,14 +43,48 @@ func TestMain(t *testing.T) {
expectedOut: "error reading YAML file(s)",
expectedError: "",
},
{
name: "ErrorLoadingConfigFile",
args: []string{"schema", "-input", "testdata/basic.yaml"},
setup: func() {
if _, err := os.Stat("schema.yaml"); err == nil {
if err := os.Rename("schema.yaml", "schema.yaml.bak"); err != nil {
log.Fatalf("Error renaming file: %v", err)
}
}

file, _ := os.Create("schema.yaml")
defer file.Close()
if _, err := file.WriteString("draft: invalid\n"); err != nil {
log.Fatalf("Error writing to file: %v", err)
}
},
cleanup: func() {
if _, err := os.Stat("schema.yaml.bak"); err == nil {
os.Remove("schema.yaml")
if err := os.Rename("schema.yaml.bak", "schema.yaml"); err != nil {
log.Fatalf("Error renaming file: %v", err)
}
} else {
os.Remove("schema.yaml")
}
},
expectedOut: "",
expectedError: "Error loading config file",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalArgs := os.Args
originalStdout := os.Stdout

defer os.Remove("values.schema.json")
if tt.setup != nil {
tt.setup()
}
if tt.cleanup != nil {
defer tt.cleanup()
}

r, w, _ := os.Pipe()
os.Stdout = w
Expand All @@ -70,9 +107,9 @@ func TestMain(t *testing.T) {

out := buf.String()

assert.Contains(t, out, tt.expectedOut)
if tt.expectedError != "" {
assert.Contains(t, out, tt.expectedError)
assert.Contains(t, out, tt.expectedError)
if tt.expectedOut != "" {
assert.Contains(t, out, tt.expectedOut)
}
})
}
Expand Down
92 changes: 81 additions & 11 deletions pkg/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,102 @@ import (
"bytes"
"flag"
"fmt"
"os"

"gopkg.in/yaml.v3"
)

// Parse flags
func ParseFlags(progname string, args []string) (config *Config, output string, err error) {
func ParseFlags(progname string, args []string) (*Config, string, error) {
flags := flag.NewFlagSet(progname, flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)

var conf Config
flags.Var(&conf.input, "input", "Multiple yaml files as inputs (comma-separated)")
flags.StringVar(&conf.outputPath, "output", "values.schema.json", "Output file path")
flags.IntVar(&conf.draft, "draft", 2020, "Draft version (4, 6, 7, 2019, or 2020)")
flags.IntVar(&conf.indent, "indent", 4, "Indentation spaces (even number)")
conf := &Config{}
flags.Var(&conf.Input, "input", "Multiple yaml files as inputs (comma-separated)")
flags.StringVar(&conf.OutputPath, "output", "values.schema.json", "Output file path")
flags.IntVar(&conf.Draft, "draft", 2020, "Draft version (4, 6, 7, 2019, or 2020)")
flags.IntVar(&conf.Indent, "indent", 4, "Indentation spaces (even number)")

// Nested SchemaRoot flags
flags.StringVar(&conf.SchemaRoot.ID, "schemaRoot.id", "", "JSON schema ID")
flags.StringVar(&conf.SchemaRoot.Title, "schemaRoot.title", "", "JSON schema title")
flags.StringVar(&conf.SchemaRoot.Description, "schemaRoot.description", "", "JSON schema description")
flags.Var(&conf.SchemaRoot.AdditionalProperties, "schemaRoot.additionalProperties", "JSON schema additional properties (true/false)")
flags.Var(&conf.SchemaRoot.AdditionalProperties, "schemaRoot.additionalProperties", "Allow additional properties")

err = flags.Parse(args)
err := flags.Parse(args)
if err != nil {
fmt.Println("Usage: helm schema [-input STR] [-draft INT] [-output STR]")
fmt.Println("Usage: helm schema [options...] <arguments>")
return nil, buf.String(), err
}

conf.args = flags.Args()
return &conf, buf.String(), nil
// Mark fields as set if they were provided as flags
flags.Visit(func(f *flag.Flag) {
switch f.Name {
case "output":
conf.OutputPathSet = true
case "draft":
conf.DraftSet = true
case "indent":
conf.IndentSet = true
}
})

conf.Args = flags.Args()
return conf, buf.String(), nil
}

// LoadConfig loads configuration from a YAML file
var readFileFunc = os.ReadFile

func LoadConfig(configPath string) (*Config, error) {
data, err := readFileFunc(configPath)
if err != nil {
if os.IsNotExist(err) {
// Return an empty config if the file does not exist
return &Config{}, nil
}
return nil, err
}

var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}

return &config, nil
}

// MergeConfig merges CLI flags into the configuration file values, giving precedence to CLI flags
func MergeConfig(fileConfig, flagConfig *Config) *Config {
mergedConfig := *fileConfig

if len(flagConfig.Input) > 0 {
mergedConfig.Input = flagConfig.Input
}
if flagConfig.OutputPathSet || mergedConfig.OutputPath == "" {
mergedConfig.OutputPath = flagConfig.OutputPath
}
if flagConfig.DraftSet || mergedConfig.Draft == 0 {
mergedConfig.Draft = flagConfig.Draft
}
if flagConfig.IndentSet || mergedConfig.Indent == 0 {
mergedConfig.Indent = flagConfig.Indent
}
if flagConfig.SchemaRoot.ID != "" {
mergedConfig.SchemaRoot.ID = flagConfig.SchemaRoot.ID
}
if flagConfig.SchemaRoot.Title != "" {
mergedConfig.SchemaRoot.Title = flagConfig.SchemaRoot.Title
}
if flagConfig.SchemaRoot.Description != "" {
mergedConfig.SchemaRoot.Description = flagConfig.SchemaRoot.Description
}
if flagConfig.SchemaRoot.AdditionalProperties.IsSet() {
mergedConfig.SchemaRoot.AdditionalProperties = flagConfig.SchemaRoot.AdditionalProperties
}
mergedConfig.Args = flagConfig.Args

return &mergedConfig
}
Loading

0 comments on commit 3682786

Please sign in to comment.