]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package config |
2 | ||
3 | import ( | |
4 | "encoding/json" | |
5 | "fmt" | |
6 | "io" | |
7 | "os" | |
8 | "path/filepath" | |
9 | "sort" | |
10 | "strings" | |
11 | ||
12 | "github.com/hashicorp/hcl" | |
13 | ) | |
14 | ||
15 | // ErrNoConfigsFound is the error returned by LoadDir if no | |
16 | // Terraform configuration files were found in the given directory. | |
17 | type ErrNoConfigsFound struct { | |
18 | Dir string | |
19 | } | |
20 | ||
21 | func (e ErrNoConfigsFound) Error() string { | |
22 | return fmt.Sprintf( | |
23 | "No Terraform configuration files found in directory: %s", | |
24 | e.Dir) | |
25 | } | |
26 | ||
27 | // LoadJSON loads a single Terraform configuration from a given JSON document. | |
28 | // | |
29 | // The document must be a complete Terraform configuration. This function will | |
30 | // NOT try to load any additional modules so only the given document is loaded. | |
31 | func LoadJSON(raw json.RawMessage) (*Config, error) { | |
32 | obj, err := hcl.Parse(string(raw)) | |
33 | if err != nil { | |
34 | return nil, fmt.Errorf( | |
35 | "Error parsing JSON document as HCL: %s", err) | |
36 | } | |
37 | ||
38 | // Start building the result | |
39 | hclConfig := &hclConfigurable{ | |
40 | Root: obj, | |
41 | } | |
42 | ||
43 | return hclConfig.Config() | |
44 | } | |
45 | ||
46 | // LoadFile loads the Terraform configuration from a given file. | |
47 | // | |
48 | // This file can be any format that Terraform recognizes, and import any | |
49 | // other format that Terraform recognizes. | |
50 | func LoadFile(path string) (*Config, error) { | |
51 | importTree, err := loadTree(path) | |
52 | if err != nil { | |
53 | return nil, err | |
54 | } | |
55 | ||
56 | configTree, err := importTree.ConfigTree() | |
57 | ||
58 | // Close the importTree now so that we can clear resources as quickly | |
59 | // as possible. | |
60 | importTree.Close() | |
61 | ||
62 | if err != nil { | |
63 | return nil, err | |
64 | } | |
65 | ||
66 | return configTree.Flatten() | |
67 | } | |
68 | ||
69 | // LoadDir loads all the Terraform configuration files in a single | |
70 | // directory and appends them together. | |
71 | // | |
72 | // Special files known as "override files" can also be present, which | |
73 | // are merged into the loaded configuration. That is, the non-override | |
74 | // files are loaded first to create the configuration. Then, the overrides | |
75 | // are merged into the configuration to create the final configuration. | |
76 | // | |
77 | // Files are loaded in lexical order. | |
78 | func LoadDir(root string) (*Config, error) { | |
79 | files, overrides, err := dirFiles(root) | |
80 | if err != nil { | |
81 | return nil, err | |
82 | } | |
83 | if len(files) == 0 { | |
84 | return nil, &ErrNoConfigsFound{Dir: root} | |
85 | } | |
86 | ||
87 | // Determine the absolute path to the directory. | |
88 | rootAbs, err := filepath.Abs(root) | |
89 | if err != nil { | |
90 | return nil, err | |
91 | } | |
92 | ||
93 | var result *Config | |
94 | ||
95 | // Sort the files and overrides so we have a deterministic order | |
96 | sort.Strings(files) | |
97 | sort.Strings(overrides) | |
98 | ||
99 | // Load all the regular files, append them to each other. | |
100 | for _, f := range files { | |
101 | c, err := LoadFile(f) | |
102 | if err != nil { | |
103 | return nil, err | |
104 | } | |
105 | ||
106 | if result != nil { | |
107 | result, err = Append(result, c) | |
108 | if err != nil { | |
109 | return nil, err | |
110 | } | |
111 | } else { | |
112 | result = c | |
113 | } | |
114 | } | |
115 | ||
116 | // Load all the overrides, and merge them into the config | |
117 | for _, f := range overrides { | |
118 | c, err := LoadFile(f) | |
119 | if err != nil { | |
120 | return nil, err | |
121 | } | |
122 | ||
123 | result, err = Merge(result, c) | |
124 | if err != nil { | |
125 | return nil, err | |
126 | } | |
127 | } | |
128 | ||
129 | // Mark the directory | |
130 | result.Dir = rootAbs | |
131 | ||
132 | return result, nil | |
133 | } | |
134 | ||
135 | // IsEmptyDir returns true if the directory given has no Terraform | |
136 | // configuration files. | |
137 | func IsEmptyDir(root string) (bool, error) { | |
138 | if _, err := os.Stat(root); err != nil && os.IsNotExist(err) { | |
139 | return true, nil | |
140 | } | |
141 | ||
142 | fs, os, err := dirFiles(root) | |
143 | if err != nil { | |
144 | return false, err | |
145 | } | |
146 | ||
147 | return len(fs) == 0 && len(os) == 0, nil | |
148 | } | |
149 | ||
150 | // Ext returns the Terraform configuration extension of the given | |
151 | // path, or a blank string if it is an invalid function. | |
152 | func ext(path string) string { | |
153 | if strings.HasSuffix(path, ".tf") { | |
154 | return ".tf" | |
155 | } else if strings.HasSuffix(path, ".tf.json") { | |
156 | return ".tf.json" | |
157 | } else { | |
158 | return "" | |
159 | } | |
160 | } | |
161 | ||
162 | func dirFiles(dir string) ([]string, []string, error) { | |
163 | f, err := os.Open(dir) | |
164 | if err != nil { | |
165 | return nil, nil, err | |
166 | } | |
167 | defer f.Close() | |
168 | ||
169 | fi, err := f.Stat() | |
170 | if err != nil { | |
171 | return nil, nil, err | |
172 | } | |
173 | if !fi.IsDir() { | |
174 | return nil, nil, fmt.Errorf( | |
175 | "configuration path must be a directory: %s", | |
176 | dir) | |
177 | } | |
178 | ||
179 | var files, overrides []string | |
180 | err = nil | |
181 | for err != io.EOF { | |
182 | var fis []os.FileInfo | |
183 | fis, err = f.Readdir(128) | |
184 | if err != nil && err != io.EOF { | |
185 | return nil, nil, err | |
186 | } | |
187 | ||
188 | for _, fi := range fis { | |
189 | // Ignore directories | |
190 | if fi.IsDir() { | |
191 | continue | |
192 | } | |
193 | ||
194 | // Only care about files that are valid to load | |
195 | name := fi.Name() | |
196 | extValue := ext(name) | |
197 | if extValue == "" || isIgnoredFile(name) { | |
198 | continue | |
199 | } | |
200 | ||
201 | // Determine if we're dealing with an override | |
202 | nameNoExt := name[:len(name)-len(extValue)] | |
203 | override := nameNoExt == "override" || | |
204 | strings.HasSuffix(nameNoExt, "_override") | |
205 | ||
206 | path := filepath.Join(dir, name) | |
207 | if override { | |
208 | overrides = append(overrides, path) | |
209 | } else { | |
210 | files = append(files, path) | |
211 | } | |
212 | } | |
213 | } | |
214 | ||
215 | return files, overrides, nil | |
216 | } | |
217 | ||
218 | // isIgnoredFile returns true or false depending on whether the | |
219 | // provided file name is a file that should be ignored. | |
220 | func isIgnoredFile(name string) bool { | |
221 | return strings.HasPrefix(name, ".") || // Unix-like hidden files | |
222 | strings.HasSuffix(name, "~") || // vim | |
223 | strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") // emacs | |
224 | } |