Skip to content

Commit b8c912f

Browse files
committed
Fix inconsistent violation reporting
Walk each data source directory returned by GetPolicy These are the actual directories (possibly symlinks) where data was downloaded Walking them directly ensures we find files even if they're symlinks
1 parent aa59de1 commit b8c912f

File tree

2 files changed

+69
-55
lines changed

2 files changed

+69
-55
lines changed

cmd/validate/vsa.go

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -947,51 +947,6 @@ func displayComponentResults(allResults []vsa.ComponentResult, data *validateVSA
947947
return nil
948948
}
949949

950-
// displayComponentResultsOld displays the validation results for each component (old implementation)
951-
// Kept temporarily for reference - will be removed after new implementation is verified
952-
func displayComponentResultsOld(allResults []vsa.ComponentResult, data *validateVSAData) {
953-
var successCount, failureCount, fallbackCount int
954-
955-
printVSAInfo(os.Stdout, "\n=== Component Validation Results ===")
956-
for _, result := range allResults {
957-
printVSAInfo(os.Stdout, fmt.Sprintf("\nComponent: %s", result.ComponentName))
958-
printVSAInfo(os.Stdout, fmt.Sprintf(" Image: %s", result.ImageRef))
959-
960-
resultType := classifyResult(result)
961-
switch resultType {
962-
case ResultTypeError:
963-
failureCount++
964-
printVSAStatus(os.Stdout, fmt.Sprintf("Failed: %v", result.Error), "failure")
965-
case ResultTypeFallback:
966-
fallbackCount++
967-
if result.FallbackResult != nil && result.FallbackResult.Component.Success {
968-
successCount++
969-
} else {
970-
failureCount++
971-
}
972-
displayFallbackResult(result, data)
973-
case ResultTypeVSASuccess:
974-
successCount++
975-
displayVSASuccessResult(result, data)
976-
case ResultTypeVSAFailure:
977-
failureCount++
978-
displayVSAFailureResult(result, data)
979-
case ResultTypeUnexpected:
980-
failureCount++
981-
printVSAStatus(os.Stdout, fmt.Sprintf(" Unexpected state: no validation result for component %s", result.ComponentName), "failure")
982-
}
983-
}
984-
985-
// Print summary statistics
986-
printVSAInfo(os.Stdout, "\n=== Snapshot Validation Summary ===")
987-
printVSAInfo(os.Stdout, fmt.Sprintf("Total components: %d", len(allResults)))
988-
printVSAInfo(os.Stdout, fmt.Sprintf("Successful: %d", successCount))
989-
printVSAInfo(os.Stdout, fmt.Sprintf("Failed: %d", failureCount))
990-
if fallbackCount > 0 {
991-
printVSAInfo(os.Stdout, fmt.Sprintf("Used fallback: %d", fallbackCount))
992-
}
993-
}
994-
995950
// displayFallbackResult displays fallback validation results
996951
// It shows VSA result first (why fallback was triggered), then the fallback image validation result
997952
func displayFallbackResult(result vsa.ComponentResult, data *validateVSAData) {
@@ -1204,12 +1159,15 @@ func validateImageFallbackWithWorkerContext(ctx context.Context, data *validateV
12041159
// Perform image validation using precomputed context and worker-specific evaluators
12051160
log.Debugf("🔄 Fallback: Starting image validation...")
12061161
log.Debugf("🔄 Fallback: Using fallback policy: %s", data.fallbackContext.FallbackPolicy)
1162+
fmt.Printf("Component: %+v\n", comp.ContainerImage)
12071163
result, err := image.ValidateImage(ctx, comp, data.snapshot, data.fallbackContext.FallbackPolicy, workerFallbackContext.Evaluators, data.info)
12081164
if err != nil {
12091165
log.Debugf("🔄 Fallback: Image validation failed with error: %v", err)
12101166
return nil, err
12111167
}
12121168

1169+
fmt.Printf("Component: %+v, violations: %+v\n", comp.ContainerImage, len(result.Violations()))
1170+
12131171
return result, nil
12141172
}
12151173

internal/evaluator/conftest_evaluator.go

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)