Skip to content

Commit

Permalink
Implement half of the CLI
Browse files Browse the repository at this point in the history
- Adds and implements --require-all and --disallow-additional-properties
- Adds checks to make sure that the input folder specified is actually a folder
- Generally tidies up some comments and minor parts of the code, such as logging.
- Refactors cmd.go into multiple functions
  • Loading branch information
AislingHPE committed Aug 23, 2024
1 parent e95a8b5 commit 4c2dc11
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 78 deletions.
1 change: 0 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ jobs:
- name: GolangCI Lint
uses: golangci/golangci-lint-action@v6
with:
# TODO: Update this version.
version: v1.60.2
args: --out-format=colored-line-number
test:
Expand Down
2 changes: 1 addition & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
- Consider making a github action for this
- Add a make rule to make a binary
- Goreleaser?
- Check github settings eg. branch protection
- slog and slog configuration (eg debug logging for condition stuff)
94 changes: 70 additions & 24 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,26 @@ var (
disallowAdditionalProperties bool
overwrite bool
allowEmpty bool
requireAll bool
outputStdOut bool
output string
input string

errReturned error
)

// rootCmd is the base command for terraschema
var rootCmd = &cobra.Command{
Use: "terraschema",
Short: "Generate JSON schema from HCL Variable Blocks in a Terraform/OpenTofu module",
Long: `TODO: Long description`,
Run: func(cmd *cobra.Command, args []string) {
path, err := filepath.Abs(input) // absolute path
if err != nil {
fmt.Printf("could not get absolute path: %v\n", err)
os.Exit(1)
}
output, err := jsonschema.CreateSchema(path, false)
if err != nil {
fmt.Printf("error creating schema: %v\n", err)
os.Exit(1)
}
jsonOutput, err := json.MarshalIndent(output, "", " ")
if err != nil {
fmt.Printf("error marshalling schema: %v\n", err)
Use: "terraschema",
Example: "terraschema -i /path/to/module -o /path/to/schema.json",
Short: "Generate JSON schema from HCL Variable Blocks in a Terraform/OpenTofu module",
Long: `TODO: Long description`,
Run: runCommand,
PostRun: func(cmd *cobra.Command, args []string) {
if errReturned != nil {
fmt.Printf("error: %v\n", errReturned)
os.Exit(1)
}

fmt.Println(string(jsonOutput))
},
}

Expand All @@ -66,13 +58,67 @@ func init() {
rootCmd.Flags().BoolVar(&outputStdOut, "stdout", false,
"output schema content to stdout instead of a file and disable error output",
)
// TODO: implement

rootCmd.Flags().BoolVar(&disallowAdditionalProperties, "disallow-additional-properties", false,
"set additionalProperties to false in the generated schema and in nested objects",
)

rootCmd.Flags().BoolVar(&allowEmpty, "allow-empty", false,
"allow empty schema if no variables are found, otherwise error",
)

rootCmd.Flags().BoolVar(&requireAll, "require-all", false,
"require all variables to be present in the schema, even if a default value is specified",
)

rootCmd.Flags().StringVarP(&input, "input", "i", ".",
"input folder containing .tf files",
)

// TODO: implement
rootCmd.Flags().BoolVar(&allowEmpty, "allow-empty", false, "allow empty schema if no variables are found, otherwise error")
rootCmd.Flags().StringVarP(&input, "input", "i", ".", "input folder containing .tf files")
// TODO: implement
rootCmd.Flags().StringVarP(&output, "output", "o", "schema.json", "output file path for schema")
rootCmd.Flags().StringVarP(&output, "output", "o", "schema.json",
"output file path for schema",
)
}

func runCommand(cmd *cobra.Command, args []string) {
path, err := filepath.Abs(input) // absolute path
if err != nil {
errReturned = fmt.Errorf("could not get absolute path for %q: %w", input, err)

return
}

folder, err := os.Stat(path)
if err != nil {
errReturned = fmt.Errorf("could not open %q: %w", path, err)

return
}

if !folder.IsDir() {
errReturned = fmt.Errorf("input %q is not a directory", path)

return
}

output, err := jsonschema.CreateSchema(path, jsonschema.CreateSchemaOptions{
RequireAll: requireAll,
AllowAdditionalProperties: !disallowAdditionalProperties,
AllowEmpty: allowEmpty,
})
if err != nil {
errReturned = fmt.Errorf("error creating schema: %w", err)

return
}

jsonOutput, err := json.MarshalIndent(output, "", " ")
if err != nil {
errReturned = fmt.Errorf("error marshalling schema: %w", err)

return
}

fmt.Println(string(jsonOutput))
}
47 changes: 33 additions & 14 deletions pkg/jsonschema/json-schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,39 @@ import (
"github.com/HewlettPackard/terraschema/pkg/reader"
)

func CreateSchema(path string, strict bool) (map[string]any, error) {
type CreateSchemaOptions struct {
RequireAll bool
AllowAdditionalProperties bool
AllowEmpty bool
}

func CreateSchema(path string, options CreateSchemaOptions) (map[string]any, error) {
schemaOut := make(map[string]any)

varMap, err := reader.GetVarMap(path)
// GetVarMaps returns an error if no .tf files are found in the directory. We
// ignore this error for now.
if err != nil && !errors.Is(err, reader.ErrFilesNotFound) {
return schemaOut, fmt.Errorf("error reading tf files at %s: %w", path, err)
if err != nil {
if errors.Is(err, reader.ErrFilesNotFound) {
if options.AllowEmpty {
fmt.Printf("No tf files were found in %q, creating empty schema\n", path)

return schemaOut, nil
}
} else {
return schemaOut, fmt.Errorf("error reading tf files at %q: %w", path, err)
}
}

if len(varMap) == 0 {
return schemaOut, nil
if options.AllowEmpty {
return schemaOut, nil
} else {
return schemaOut, errors.New("no variables found in tf files")
}
}

schemaOut["$schema"] = "http://json-schema.org/draft-07/schema#"

if strict {
if !options.AllowAdditionalProperties {
schemaOut["additionalProperties"] = false
} else {
schemaOut["additionalProperties"] = true
Expand All @@ -35,12 +51,15 @@ func CreateSchema(path string, strict bool) (map[string]any, error) {
properties := make(map[string]any)
requiredArray := []any{}
for name, variable := range varMap {
if variable.Required {
if variable.Required && !options.RequireAll {
requiredArray = append(requiredArray, name)
}
if options.RequireAll {
requiredArray = append(requiredArray, name)
}
node, err := createNode(name, variable, strict)
node, err := createNode(name, variable, options)
if err != nil {
return schemaOut, fmt.Errorf("error creating node for %s: %w", name, err)
return schemaOut, fmt.Errorf("error creating node for %q: %w", name, err)
}

properties[name] = node
Expand All @@ -54,16 +73,16 @@ func CreateSchema(path string, strict bool) (map[string]any, error) {
return schemaOut, nil
}

func createNode(name string, v model.TranslatedVariable, strict bool) (map[string]any, error) {
func createNode(name string, v model.TranslatedVariable, options CreateSchemaOptions) (map[string]any, error) {
tc, err := reader.GetTypeConstraint(v.Variable.Type)
if err != nil {
return nil, fmt.Errorf("getting type constraint for %s: %w", name, err)
return nil, fmt.Errorf("getting type constraint for %q: %w", name, err)
}

nullableIsTrue := v.Variable.Nullable != nil && *v.Variable.Nullable
node, err := getNodeFromType(name, tc, nullableIsTrue, strict)
node, err := getNodeFromType(name, tc, nullableIsTrue, options)
if err != nil {
return nil, fmt.Errorf("%s: %w", name, err)
return nil, fmt.Errorf("%q: %w", name, err)
}

if v.Variable.Default != nil {
Expand Down
10 changes: 7 additions & 3 deletions pkg/jsonschema/json-schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ func TestCreateSchema(t *testing.T) {
expected, err := os.ReadFile(filepath.Join(schemaPath, name, "schema.json"))
require.NoError(t, err)

result, err := CreateSchema(filepath.Join(tfPath, name), false)
result, err := CreateSchema(filepath.Join(tfPath, name), CreateSchemaOptions{
RequireAll: false,
AllowAdditionalProperties: true,
AllowEmpty: true,
})
require.NoError(t, err)

var expectedMap map[string]interface{}
var expectedMap map[string]any
err = json.Unmarshal(expected, &expectedMap)
require.NoError(t, err)

Expand Down Expand Up @@ -283,7 +287,7 @@ func TestSampleInput(t *testing.T) {

input, err := os.ReadFile(tc.filePath)
require.NoError(t, err)
var m interface{}
var m any
err = json.Unmarshal(input, &m)
require.NoError(t, err)

Expand Down
Loading

0 comments on commit 4c2dc11

Please sign in to comment.