package config import ( "bufio" "fmt" "io" "os" "github.com/hashicorp/errwrap" ) // configurable is an interface that must be implemented by any configuration // formats of Terraform in order to return a *Config. type configurable interface { Config() (*Config, error) } // importTree is the result of the first-pass load of the configuration // files. It is a tree of raw configurables and then any children (their // imports). // // An importTree can be turned into a configTree. type importTree struct { Path string Raw configurable Children []*importTree } // This is the function type that must be implemented by the configuration // file loader to turn a single file into a configurable and any additional // imports. type fileLoaderFunc func(path string) (configurable, []string, error) // Set this to a non-empty value at link time to enable the HCL2 experiment. // This is not currently enabled for release builds. // // For example: // go install -ldflags="-X github.com/hashicorp/terraform/config.enableHCL2Experiment=true" github.com/hashicorp/terraform var enableHCL2Experiment = "" // loadTree takes a single file and loads the entire importTree for that // file. This function detects what kind of configuration file it is an // executes the proper fileLoaderFunc. func loadTree(root string) (*importTree, error) { var f fileLoaderFunc // HCL2 experiment is currently activated at build time via the linker. // See the comment on this variable for more information. if enableHCL2Experiment == "" { // Main-line behavior: always use the original HCL parser switch ext(root) { case ".tf", ".tf.json": f = loadFileHcl default: } } else { // Experimental behavior: use the HCL2 parser if the opt-in comment // is present. switch ext(root) { case ".tf": // We need to sniff the file for the opt-in comment line to decide // if the file is participating in the HCL2 experiment. cf, err := os.Open(root) if err != nil { return nil, err } sc := bufio.NewScanner(cf) for sc.Scan() { if sc.Text() == "#terraform:hcl2" { f = globalHCL2Loader.loadFile } } if f == nil { f = loadFileHcl } case ".tf.json": f = loadFileHcl default: } } if f == nil { return nil, fmt.Errorf( "%s: unknown configuration format. Use '.tf' or '.tf.json' extension", root) } c, imps, err := f(root) if err != nil { return nil, err } children := make([]*importTree, len(imps)) for i, imp := range imps { t, err := loadTree(imp) if err != nil { return nil, err } children[i] = t } return &importTree{ Path: root, Raw: c, Children: children, }, nil } // Close releases any resources we might be holding open for the importTree. // // This can safely be called even while ConfigTree results are alive. The // importTree is not bound to these. func (t *importTree) Close() error { if c, ok := t.Raw.(io.Closer); ok { c.Close() } for _, ct := range t.Children { ct.Close() } return nil } // ConfigTree traverses the importTree and turns each node into a *Config // object, ultimately returning a *configTree. func (t *importTree) ConfigTree() (*configTree, error) { config, err := t.Raw.Config() if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Error loading %s: {{err}}", t.Path), err) } // Build our result result := &configTree{ Path: t.Path, Config: config, } // Build the config trees for the children result.Children = make([]*configTree, len(t.Children)) for i, ct := range t.Children { t, err := ct.ConfigTree() if err != nil { return nil, err } result.Children[i] = t } return result, nil }