ee147d9ee6
This is essentially a CLI wrapper around the hcldec package, accepting a decoding specification via a HCL-based language and using it to translate input HCL files into JSON values while performing basic structural and type validation of the input files.
215 lines
4.6 KiB
Go
215 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/hashicorp/hcl2/hcldec"
|
|
"github.com/hashicorp/hcl2/hclparse"
|
|
flag "github.com/spf13/pflag"
|
|
"github.com/zclconf/go-cty/cty"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const versionStr = "0.0.1-dev"
|
|
|
|
// vars is populated from --vars arguments on the command line, via a flag
|
|
// registration in init() below.
|
|
var vars = &varSpecs{}
|
|
|
|
var (
|
|
specFile = flag.StringP("spec", "s", "", "path to spec file (required)")
|
|
outputFile = flag.StringP("out", "o", "", "write to the given file, instead of stdout")
|
|
showVersion = flag.BoolP("version", "v", false, "show the version number and immediately exit")
|
|
)
|
|
|
|
var parser = hclparse.NewParser()
|
|
var diagWr hcl.DiagnosticWriter // initialized in init
|
|
|
|
func init() {
|
|
flag.VarP(vars, "vars", "V", "provide variables to the given configuration file(s)")
|
|
|
|
color := terminal.IsTerminal(int(os.Stderr.Fd()))
|
|
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
w = 80
|
|
}
|
|
diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(w), color)
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
if *showVersion {
|
|
fmt.Println(versionStr)
|
|
os.Exit(0)
|
|
}
|
|
|
|
args := flag.Args()
|
|
|
|
err := realmain(args)
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func realmain(args []string) error {
|
|
|
|
if *specFile == "" {
|
|
return fmt.Errorf("the --spec=... argument is required")
|
|
}
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
spec, specDiags := loadSpecFile(*specFile)
|
|
diags = append(diags, specDiags...)
|
|
if specDiags.HasErrors() {
|
|
diagWr.WriteDiagnostics(diags)
|
|
os.Exit(2)
|
|
}
|
|
|
|
var ctx *hcl.EvalContext
|
|
if len(*vars) != 0 {
|
|
ctx = &hcl.EvalContext{
|
|
Variables: map[string]cty.Value{},
|
|
}
|
|
for i, varsSpec := range *vars {
|
|
var vals map[string]cty.Value
|
|
var valsDiags hcl.Diagnostics
|
|
if strings.HasPrefix(strings.TrimSpace(varsSpec), "{") {
|
|
// literal JSON object on the command line
|
|
vals, valsDiags = parseVarsArg(varsSpec, i)
|
|
} else {
|
|
// path to a file containing either HCL or JSON (by file extension)
|
|
vals, valsDiags = parseVarsFile(varsSpec)
|
|
}
|
|
diags = append(diags, valsDiags...)
|
|
for k, v := range vals {
|
|
ctx.Variables[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
var bodies []hcl.Body
|
|
|
|
if len(args) == 0 {
|
|
src, err := ioutil.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read stdin: %s", err)
|
|
}
|
|
|
|
f, fDiags := parser.ParseHCL(src, "<stdin>")
|
|
diags = append(diags, fDiags...)
|
|
if !fDiags.HasErrors() {
|
|
bodies = append(bodies, f.Body)
|
|
}
|
|
} else {
|
|
for _, filename := range args {
|
|
f, fDiags := parser.ParseHCLFile(filename)
|
|
diags = append(diags, fDiags...)
|
|
if !fDiags.HasErrors() {
|
|
bodies = append(bodies, f.Body)
|
|
}
|
|
}
|
|
}
|
|
|
|
if diags.HasErrors() {
|
|
diagWr.WriteDiagnostics(diags)
|
|
os.Exit(2)
|
|
}
|
|
|
|
var body hcl.Body
|
|
switch len(bodies) {
|
|
case 0:
|
|
// should never happen, but... okay?
|
|
body = hcl.EmptyBody()
|
|
case 1:
|
|
body = bodies[0]
|
|
default:
|
|
body = hcl.MergeBodies(bodies)
|
|
}
|
|
|
|
val, decDiags := hcldec.Decode(body, spec, ctx)
|
|
diags = append(diags, decDiags...)
|
|
|
|
if diags.HasErrors() {
|
|
diagWr.WriteDiagnostics(diags)
|
|
os.Exit(2)
|
|
}
|
|
|
|
out, err := ctyjson.Marshal(val, val.Type())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// hcldec will include explicit nulls where an ObjectSpec has a spec
|
|
// that refers to a missing item, but that'll probably be annoying for
|
|
// a consumer of our output to deal with so we'll just strip those
|
|
// out and reduce to only the non-null values.
|
|
out = stripJSONNullProperties(out)
|
|
|
|
target := os.Stdout
|
|
if *outputFile != "" {
|
|
target, err = os.OpenFile(*outputFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("can't open %s for writing: %s", *outputFile, err)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(target, "%s\n", out)
|
|
|
|
return nil
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr, "usage: hcldec --spec=<spec-file> [options] [hcl-file ...]\n")
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
|
|
func stripJSONNullProperties(src []byte) []byte {
|
|
var v interface{}
|
|
err := json.Unmarshal(src, &v)
|
|
if err != nil {
|
|
// We expect valid JSON
|
|
panic(err)
|
|
}
|
|
|
|
v = stripNullMapElements(v)
|
|
|
|
new, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return new
|
|
}
|
|
|
|
func stripNullMapElements(v interface{}) interface{} {
|
|
switch tv := v.(type) {
|
|
case map[string]interface{}:
|
|
for k, ev := range tv {
|
|
if ev == nil {
|
|
delete(tv, k)
|
|
} else {
|
|
tv[k] = stripNullMapElements(ev)
|
|
}
|
|
}
|
|
return v
|
|
case []interface{}:
|
|
for i, ev := range tv {
|
|
tv[i] = stripNullMapElements(ev)
|
|
}
|
|
return v
|
|
default:
|
|
return v
|
|
}
|
|
}
|