1package config 2 3import ( 4 "fmt" 5 "io/fs" 6 "path/filepath" 7 "strings" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/hclparse" 11 "github.com/spf13/afero" 12 "github.com/zclconf/go-cty/cty" 13 "github.com/zclconf/go-cty/cty/function" 14) 15 16type SourceType string 17 18const ( 19 SourceJSON = "json" 20 SourceHCL = "hcl" 21) 22 23// EnvVarPrefix is a prefix for environment variable names to be exported for HCL substitution. 24const EnvVarPrefix = "CQ_VAR_" 25 26// Parser is the main interface to read configuration files and other related 27// files from disk. 28// 29// It retains a cache of all files that are loaded so that they can be used 30// to create source code snippets in diagnostics, etc. 31type Parser struct { 32 fs afero.Afero 33 p *hclparse.Parser 34 HCLContext hcl.EvalContext 35} 36 37type Option func(*Parser) 38 39func WithFS(fs afero.Fs) Option { 40 return func(p *Parser) { 41 p.fs = afero.Afero{Fs: fs} 42 } 43} 44 45// WithEnvironmentVariables fills hcl.Context with values of environment variables given in vars. 46// Only variables that start with given prefix are considered. Prefix is removed from the name and 47// the name is lower cased then. 48func WithEnvironmentVariables(prefix string, vars []string) Option { 49 return func(p *Parser) { 50 EnvToHCLContext(&p.HCLContext, prefix, vars) 51 } 52} 53 54// NewParser creates and returns a new Parser. 55func NewParser(options ...Option) *Parser { 56 p := Parser{ 57 fs: afero.Afero{Fs: afero.OsFs{}}, 58 p: hclparse.NewParser(), 59 HCLContext: hcl.EvalContext{ 60 Variables: make(map[string]cty.Value), 61 Functions: make(map[string]function.Function), 62 }, 63 } 64 65 for _, opt := range options { 66 opt(&p) 67 } 68 return &p 69} 70 71// LoadHCLFile is a low-level method that reads the file at the given path, 72// parses it, and returns the hcl.Body representing its root. In many cases 73// it is better to use one of the other Load*File methods on this type, 74// which additionally decode the root body in some way and return a higher-level 75// construct. 76// 77// If the file cannot be read at all -- e.g. because it does not exist -- then 78// this method will return a nil body and error diagnostics. In this case 79// callers may wish to ignore the provided error diagnostics and produce 80// a more context-sensitive error instead. 81// 82// The file will be parsed using the HCL native syntax unless the filename 83// ends with ".json", in which case the HCL JSON syntax will be used. 84func (p *Parser) LoadHCLFile(path string) (hcl.Body, hcl.Diagnostics) { 85 src, err := p.fs.ReadFile(path) 86 87 if err != nil { 88 if e, ok := err.(*fs.PathError); ok { 89 err = fmt.Errorf(e.Err.Error()) 90 } 91 return nil, hcl.Diagnostics{ 92 { 93 Severity: hcl.DiagError, 94 Summary: "Failed to read file", 95 Detail: fmt.Sprintf("The file %q could not be read: %s", path, err), 96 }, 97 } 98 } 99 return p.loadFromSource(path, src, SourceType(filepath.Ext(path))) 100} 101 102func (p *Parser) loadFromSource(name string, data []byte, ext SourceType) (hcl.Body, hcl.Diagnostics) { 103 var file *hcl.File 104 var diags hcl.Diagnostics 105 switch ext { 106 case SourceJSON: 107 file, diags = p.p.ParseJSON(data, name) 108 default: 109 file, diags = p.p.ParseHCL(data, name) 110 } 111 // If the returned file or body is nil, then we'll return a non-nil empty 112 // body so we'll meet our contract that nil means an error reading the file. 113 if file == nil || file.Body == nil { 114 return hcl.EmptyBody(), diags 115 } 116 117 return file.Body, diags 118} 119 120func (p *Parser) LoadFromSource(name string, data []byte, ext SourceType) (hcl.Body, hcl.Diagnostics) { 121 return p.loadFromSource(name, data, ext) 122} 123 124func EnvToHCLContext(evalContext *hcl.EvalContext, prefix string, vars []string) { 125 for _, e := range vars { 126 pair := strings.SplitN(e, "=", 2) 127 if strings.HasPrefix(pair[0], prefix) { 128 evalContext.Variables[strings.TrimPrefix(pair[0], prefix)] = cty.StringVal(pair[1]) 129 } 130 } 131} 132