package config import ( "encoding/json" "fmt" "io" "os" "path/filepath" "sort" "strings" "github.com/hashicorp/hcl" ) // ErrNoConfigsFound is the error returned by LoadDir if no // Terraform configuration files were found in the given directory. type ErrNoConfigsFound struct { Dir string } func (e ErrNoConfigsFound) Error() string { return fmt.Sprintf( "No Terraform configuration files found in directory: %s", e.Dir) } // LoadJSON loads a single Terraform configuration from a given JSON document. // // The document must be a complete Terraform configuration. This function will // NOT try to load any additional modules so only the given document is loaded. func LoadJSON(raw json.RawMessage) (*Config, error) { obj, err := hcl.Parse(string(raw)) if err != nil { return nil, fmt.Errorf( "Error parsing JSON document as HCL: %s", err) } // Start building the result hclConfig := &hclConfigurable{ Root: obj, } return hclConfig.Config() } // LoadFile loads the Terraform configuration from a given file. // // This file can be any format that Terraform recognizes, and import any // other format that Terraform recognizes. func LoadFile(path string) (*Config, error) { importTree, err := loadTree(path) if err != nil { return nil, err } configTree, err := importTree.ConfigTree() // Close the importTree now so that we can clear resources as quickly // as possible. importTree.Close() if err != nil { return nil, err } return configTree.Flatten() } // LoadDir loads all the Terraform configuration files in a single // directory and appends them together. // // Special files known as "override files" can also be present, which // are merged into the loaded configuration. That is, the non-override // files are loaded first to create the configuration. Then, the overrides // are merged into the configuration to create the final configuration. // // Files are loaded in lexical order. func LoadDir(root string) (*Config, error) { files, overrides, err := dirFiles(root) if err != nil { return nil, err } if len(files) == 0 && len(overrides) == 0 { return nil, &ErrNoConfigsFound{Dir: root} } // Determine the absolute path to the directory. rootAbs, err := filepath.Abs(root) if err != nil { return nil, err } var result *Config // Sort the files and overrides so we have a deterministic order sort.Strings(files) sort.Strings(overrides) // Load all the regular files, append them to each other. for _, f := range files { c, err := LoadFile(f) if err != nil { return nil, err } if result != nil { result, err = Append(result, c) if err != nil { return nil, err } } else { result = c } } if len(files) == 0 { result = &Config{} } // Load all the overrides, and merge them into the config for _, f := range overrides { c, err := LoadFile(f) if err != nil { return nil, err } result, err = Merge(result, c) if err != nil { return nil, err } } // Mark the directory result.Dir = rootAbs return result, nil } // IsEmptyDir returns true if the directory given has no Terraform // configuration files. func IsEmptyDir(root string) (bool, error) { if _, err := os.Stat(root); err != nil && os.IsNotExist(err) { return true, nil } fs, os, err := dirFiles(root) if err != nil { return false, err } return len(fs) == 0 && len(os) == 0, nil } // Ext returns the Terraform configuration extension of the given // path, or a blank string if it is an invalid function. func ext(path string) string { if strings.HasSuffix(path, ".tf") { return ".tf" } else if strings.HasSuffix(path, ".tf.json") { return ".tf.json" } else { return "" } } func dirFiles(dir string) ([]string, []string, error) { f, err := os.Open(dir) if err != nil { return nil, nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, nil, err } if !fi.IsDir() { return nil, nil, fmt.Errorf( "configuration path must be a directory: %s", dir) } var files, overrides []string err = nil for err != io.EOF { var fis []os.FileInfo fis, err = f.Readdir(128) if err != nil && err != io.EOF { return nil, nil, err } for _, fi := range fis { // Ignore directories if fi.IsDir() { continue } // Only care about files that are valid to load name := fi.Name() extValue := ext(name) if extValue == "" || IsIgnoredFile(name) { continue } // Determine if we're dealing with an override nameNoExt := name[:len(name)-len(extValue)] override := nameNoExt == "override" || strings.HasSuffix(nameNoExt, "_override") path := filepath.Join(dir, name) if override { overrides = append(overrides, path) } else { files = append(files, path) } } } return files, overrides, nil } // IsIgnoredFile returns true or false depending on whether the // provided file name is a file that should be ignored. func IsIgnoredFile(name string) bool { return strings.HasPrefix(name, ".") || // Unix-like hidden files strings.HasSuffix(name, "~") || // vim strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") // emacs }