|
1 | 1 | package hclparse |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "fmt" |
4 | 5 | iofs "io/fs" |
5 | 6 | "path/filepath" |
6 | 7 |
|
@@ -179,53 +180,89 @@ func BuildAutoIncludeEvalContext(unitRefs, stackRefs []ComponentRef) *hcl.EvalCo |
179 | 180 | // AutoIncludeDependencyPaths reads the terragrunt.autoinclude.hcl file in |
180 | 181 | // unitDir and returns resolved dependency config_path values. |
181 | 182 | // Returns (nil, nil) if the file does not exist or has no dependencies. |
| 183 | +// Panics when fs is nil (programmer error). Returns EmptyArgError when unitDir |
| 184 | +// is empty so callers can distinguish bad input from a missing file. |
182 | 185 | func AutoIncludeDependencyPaths(fs vfs.FS, unitDir string) ([]string, error) { |
| 186 | + if fs == nil { |
| 187 | + panic(fmt.Sprintf("hclparse.AutoIncludeDependencyPaths: fs is nil (unitDir=%q)", unitDir)) |
| 188 | + } |
| 189 | + |
| 190 | + if unitDir == "" { |
| 191 | + return nil, EmptyArgError{Func: "AutoIncludeDependencyPaths", Arg: "unitDir"} |
| 192 | + } |
| 193 | + |
183 | 194 | unitDir = util.ResolvePath(unitDir) |
184 | 195 | autoIncludePath := filepath.Join(unitDir, AutoIncludeFile) |
185 | 196 |
|
186 | | - data, err := vfs.ReadFile(fs, autoIncludePath) |
| 197 | + body, err := readAutoIncludeBody(fs, autoIncludePath) |
| 198 | + if err != nil || body == nil { |
| 199 | + return nil, err |
| 200 | + } |
| 201 | + |
| 202 | + paths := make([]string, 0, len(body.Blocks)) |
| 203 | + |
| 204 | + for _, block := range body.Blocks { |
| 205 | + if depPath, ok := extractDependencyConfigPath(block, unitDir); ok { |
| 206 | + paths = append(paths, depPath) |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + return paths, nil |
| 211 | +} |
| 212 | + |
| 213 | +// readAutoIncludeBody reads and parses the autoinclude file at path. |
| 214 | +// Returns (nil, nil) when the file does not exist. |
| 215 | +func readAutoIncludeBody(fs vfs.FS, path string) (*hclsyntax.Body, error) { |
| 216 | + data, err := vfs.ReadFile(fs, path) |
187 | 217 | if errors.Is(err, iofs.ErrNotExist) { |
188 | 218 | return nil, nil |
189 | 219 | } |
190 | 220 |
|
191 | 221 | if err != nil { |
192 | | - return nil, FileReadError{FilePath: autoIncludePath, Err: err} |
| 222 | + return nil, FileReadError{FilePath: path, Err: err} |
193 | 223 | } |
194 | 224 |
|
195 | | - file, diags := hclsyntax.ParseConfig(data, autoIncludePath, hcl.Pos{Line: 1, Column: 1}) |
| 225 | + file, diags := hclsyntax.ParseConfig(data, path, hcl.Pos{Line: 1, Column: 1}) |
196 | 226 | if diags.HasErrors() { |
197 | | - return nil, FileParseError{FilePath: autoIncludePath, Detail: diags.Error()} |
| 227 | + return nil, FileParseError{FilePath: path, Detail: diags.Error()} |
198 | 228 | } |
199 | 229 |
|
200 | 230 | body, ok := file.Body.(*hclsyntax.Body) |
201 | 231 | if !ok { |
202 | | - return nil, UnexpectedBodyTypeError{FilePath: autoIncludePath} |
| 232 | + return nil, UnexpectedBodyTypeError{FilePath: path} |
203 | 233 | } |
204 | 234 |
|
205 | | - var paths []string |
206 | | - |
207 | | - for _, block := range body.Blocks { |
208 | | - if block.Type != blockDependency || len(block.Labels) == 0 { |
209 | | - continue |
210 | | - } |
| 235 | + return body, nil |
| 236 | +} |
211 | 237 |
|
212 | | - configPathAttr, exists := block.Body.Attributes[attrConfigPath] |
213 | | - if !exists { |
214 | | - continue |
215 | | - } |
| 238 | +// extractDependencyConfigPath returns the resolved absolute config_path for a |
| 239 | +// dependency block, or ("", false) if the block is not a valid dependency or |
| 240 | +// config_path cannot be evaluated to a string. |
| 241 | +// |
| 242 | +// The nil eval context passed to Expr.Value is intentional: GenerateAutoIncludeFile |
| 243 | +// always writes config_path as a literal quoted string via writeDependencyBlock |
| 244 | +// (see generate.go), so no variable resolution is required. If that contract is |
| 245 | +// ever relaxed to emit interpolations, callers must pass a real eval context |
| 246 | +// here or the dependency will be silently dropped from the DAG. |
| 247 | +func extractDependencyConfigPath(block *hclsyntax.Block, unitDir string) (string, bool) { |
| 248 | + if block.Type != blockDependency || len(block.Labels) == 0 { |
| 249 | + return "", false |
| 250 | + } |
216 | 251 |
|
217 | | - val, valDiags := configPathAttr.Expr.Value(nil) |
218 | | - if valDiags.HasErrors() || !val.IsKnown() || val.IsNull() || val.Type() != cty.String { |
219 | | - continue |
220 | | - } |
| 252 | + configPathAttr, exists := block.Body.Attributes[attrConfigPath] |
| 253 | + if !exists { |
| 254 | + return "", false |
| 255 | + } |
221 | 256 |
|
222 | | - depPath := val.AsString() |
223 | | - if !filepath.IsAbs(depPath) { |
224 | | - depPath = filepath.Clean(filepath.Join(unitDir, depPath)) |
225 | | - } |
| 257 | + val, diags := configPathAttr.Expr.Value(nil) |
| 258 | + if diags.HasErrors() || !val.IsKnown() || val.IsNull() || val.Type() != cty.String { |
| 259 | + return "", false |
| 260 | + } |
226 | 261 |
|
227 | | - paths = append(paths, util.ResolvePath(depPath)) |
| 262 | + depPath := val.AsString() |
| 263 | + if !filepath.IsAbs(depPath) { |
| 264 | + depPath = filepath.Clean(filepath.Join(unitDir, depPath)) |
228 | 265 | } |
229 | 266 |
|
230 | | - return paths, nil |
| 267 | + return util.ResolvePath(depPath), true |
231 | 268 | } |
0 commit comments