@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "encoding/json"
2222 "fmt"
23+ "io/fs"
2324 "net/url"
2425 "os"
2526 "path"
@@ -441,6 +442,8 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
441442 rules := policyRules {}
442443 // Track non-annotated rules separately for filtering purposes only
443444 nonAnnotatedRules := nonAnnotatedRules {}
445+ // Track data source directories for prepareDataDirs
446+ dataSourceDirs := []string {}
444447 // Download all sources
445448 for _ , s := range c .policySources {
446449 dir , err := s .GetPolicy (ctx , c .workDir , false )
@@ -449,6 +452,11 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
449452 // TODO do we want to download other policies instead of erroring out?
450453 return nil , err
451454 }
455+ // Track data source directories - these are the actual directories returned by GetPolicy
456+ // which may be symlinks, so we need to use them directly rather than walking
457+ if s .Subdir () == "data" {
458+ dataSourceDirs = append (dataSourceDirs , dir )
459+ }
452460 annotations := []* ast.AnnotationsRef {}
453461 fs := utils .FS (ctx )
454462 // We only want to inspect the directory of policy subdirs, not config or data subdirs.
@@ -584,13 +592,11 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
584592 log .Debugf ("Namespaces to use: %v, allNamespaces: %v" , namespacesToUse , allNamespaces )
585593
586594 // Prepare the list of data dirs
587- dataDirs , err := c .prepareDataDirs (ctx )
595+ dataDirs , err := c .prepareDataDirs (ctx , dataSourceDirs )
588596 if err != nil {
589597 return nil , err
590598 }
591599
592- log .Debugf ("Data dirs: %v" , dataDirs )
593-
594600 r = & conftestRunner {
595601 runner.TestRunner {
596602 Data : dataDirs ,
@@ -700,15 +706,54 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
700706 return results , nil
701707}
702708
703- // prepareDataDirs inspects the top level data dir and returns a list of the directories
704- // that appear to have data files in them. That list will be passed to the conftest runner.
705- func (c conftestEvaluator ) prepareDataDirs (ctx context.Context ) ([]string , error ) {
709+ // prepareDataDirs inspects the data source directories and base data dir to return a list of
710+ // directories that contain data files. That list will be passed to the conftest runner.
711+ // dataSourceDirs contains the directories returned by GetPolicy for data sources.
712+ // These directories may be symlinks (from cached downloads), but we walk them directly
713+ // which ensures we find the files regardless of whether they're symlinks or not.
714+ func (c conftestEvaluator ) prepareDataDirs (ctx context.Context , dataSourceDirs []string ) ([]string , error ) {
706715 // The reason we do this is to avoid having the names of the subdirs under c.dataDir
707716 // converted to keys in the data structure. We want the top level keys in the data files
708717 // to be at the top level of the data structure visible to the rego rules.
709718
710719 dirsWithDataFiles := make (map [string ]bool )
711- err := afero .Walk (c .fs , c .dataDir , func (path string , info os.FileInfo , err error ) error {
720+
721+ // Walk each data source directory returned by GetPolicy
722+ // These are the actual directories (possibly symlinks) where data was downloaded
723+ // Walking them directly ensures we find files even if they're symlinks
724+ for _ , dataSourceDir := range dataSourceDirs {
725+ // IMPORTANT: Use fs.WalkDir instead of afero.Walk because afero.Walk does not follow symlinks.
726+ // When cached downloads create symlinks (as in getPolicyThroughCache), afero.Walk won't traverse
727+ // into the symlinked directories, causing data files to be missed.
728+ // See afero issue https://github.com/spf13/afero/issues/284 and internal/opa/inspect.go for reference.
729+ err := fs .WalkDir (opaWrapperFs {afs : c .fs }, dataSourceDir , func (path string , d fs.DirEntry , err error ) error {
730+ if err != nil {
731+ return err
732+ }
733+
734+ // Only process files, not directories
735+ if ! d .IsDir () {
736+ ext := filepath .Ext (d .Name ())
737+ // Check if this is a data file (.json, .yaml, .yml)
738+ // Todo: Should probably recognize other supported types of data
739+ if ext == ".json" || ext == ".yaml" || ext == ".yml" {
740+ // Mark the directory containing this file as having data
741+ dir := filepath .Dir (path )
742+ dirsWithDataFiles [dir ] = true
743+ }
744+ }
745+
746+ return nil
747+ })
748+ if err != nil {
749+ // Continue with other directories even if one fails
750+ continue
751+ }
752+ }
753+
754+ // Also walk the base data directory to find config.json and any other files
755+ // This ensures we don't miss files that weren't from GetPolicy sources
756+ err := fs .WalkDir (opaWrapperFs {afs : c .fs }, c .dataDir , func (path string , d fs.DirEntry , err error ) error {
712757 if err != nil {
713758 return err
714759 }
@@ -719,10 +764,9 @@ func (c conftestEvaluator) prepareDataDirs(ctx context.Context) ([]string, error
719764 }
720765
721766 // Only process files, not directories
722- if ! info .IsDir () {
723- ext := filepath .Ext (info .Name ())
767+ if ! d .IsDir () {
768+ ext := filepath .Ext (d .Name ())
724769 // Check if this is a data file (.json, .yaml, .yml)
725- // Todo: Should probably recognize other supported types of data
726770 if ext == ".json" || ext == ".yaml" || ext == ".yml" {
727771 // Mark the directory containing this file as having data
728772 dir := filepath .Dir (path )
@@ -1228,3 +1272,15 @@ func extractCodeFromRuleBody(ruleRef *ast.Rule) string {
12281272
12291273 return ""
12301274}
1275+
1276+ // opaWrapperFs wraps afero.Fs to implement fs.FS interface for use with fs.WalkDir.
1277+ // This ensures symlinks are followed when walking directories, which is critical
1278+ // when cached downloads create symlinks via getPolicyThroughCache.
1279+ // See internal/opa/inspect.go for the original implementation.
1280+ type opaWrapperFs struct {
1281+ afs afero.Fs
1282+ }
1283+
1284+ func (w opaWrapperFs ) Open (name string ) (fs.File , error ) {
1285+ return w .afs .Open (name )
1286+ }
0 commit comments