Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: read configuration from file #77

Merged
merged 5 commits into from
Jun 24, 2024
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
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