github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/bobtask/target/verify.go (about)

     1  package target
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/benchkram/bob/pkg/boblog"
    10  	"github.com/benchkram/bob/pkg/filehash"
    11  )
    12  
    13  // Hint: comparing the modification time is tricky as a artifact extraction
    14  // from a tar archive changes the modification time of a file.
    15  
    16  // VerifyShallow compare targets against an existing
    17  // buildinfo. It will only check if the size of the files changed.
    18  // Docker targets are verified similarly as in plain verify
    19  // as there is no performance penalty.
    20  // In case the expected buildinfo does not exist Verify checks against filesystemEntriesRaw.
    21  func (t *T) VerifyShallow() VerifyResult {
    22  	r := NewVerifyResult()
    23  	r.TargetIsValid = t.verifyFilesystemShallow(&r) && t.verifyDocker()
    24  	return r
    25  }
    26  
    27  // VerifyResult is the result of a target verify call.
    28  // It tells if the target is valid and if not InvalidFiles
    29  // will contain the list of invalid files along with their reason.
    30  // A file can be invalid for multiple reasons. ex. a changed file
    31  // is invalid because of size and content hash.
    32  // The map of invalid files can be used to extract only
    33  // invalidated files from an artifact.
    34  type VerifyResult struct {
    35  	// TargetIsValid shows if target is valid or not
    36  	TargetIsValid bool
    37  	// InvalidFiles maps filePath to reasons why it's invalid
    38  	InvalidFiles map[string][]Reason
    39  }
    40  
    41  // NewVerifyResult initializes a new VerifyResult
    42  func NewVerifyResult() VerifyResult {
    43  	var v VerifyResult
    44  	v.InvalidFiles = make(map[string][]Reason)
    45  	return v
    46  }
    47  
    48  // AddInvalidReason adds a reason for invalidation to a certain filePath
    49  func (v VerifyResult) AddInvalidReason(filePath string, reason Reason) {
    50  	v.InvalidFiles[filePath] = append(v.InvalidFiles[filePath], reason)
    51  }
    52  
    53  // Reason contains the reason why a file/directory makes the target invalid
    54  type Reason string
    55  
    56  const (
    57  	ReasonCreatedAfterBuild Reason = "CREATED-AFTER-BUILD"
    58  	ReasonSizeChanged       Reason = "SIZE-CHANGED"
    59  	ReasonHashChanged       Reason = "HASH-CHANGED"
    60  	ReasonMissing           Reason = "MISSING"
    61  	ReasonForcedByNoCache   Reason = "FORCED-BY-NO-CACHE"
    62  )
    63  
    64  // Verify existence and integrity of targets against an expected buildinfo.
    65  // In case the expected buildinfo does not exist Verify checks against filesystemEntriesRaw.
    66  //
    67  // Verify returns true when no targets are defined.
    68  // Verify returns when there is nothing to compare against.
    69  func (t *T) Verify() bool {
    70  	return t.verifyFilesystem() && t.verifyDocker()
    71  }
    72  
    73  func (t *T) preConditionsFilesystem() bool {
    74  	if len(*t.filesystemEntries) == 0 && len(t.filesystemEntriesRaw) == 0 {
    75  		return true
    76  	}
    77  
    78  	// In case there was NO previous local build
    79  	// verify returns false indicating that there can't
    80  	// exist a valid target from a previous build.
    81  	// Loading from the cache must be handled by the calling function.
    82  	if t.expected == nil {
    83  		return false
    84  	}
    85  
    86  	// This usually indicates a file was added/removed manually
    87  	// from a target directory.
    88  	if len(t.expected.Filesystem.Files) != len(*t.filesystemEntries) {
    89  		return false
    90  	}
    91  
    92  	return true
    93  }
    94  
    95  // verifyFilesystemShallow verifies a filesystem target
    96  func (t *T) verifyFilesystemShallow(v *VerifyResult) bool {
    97  	if t.filesystemEntries == nil {
    98  		return true
    99  	}
   100  	if t.expected == nil {
   101  		return true
   102  	}
   103  
   104  	// create map to optimise access when checking for
   105  	// missing files
   106  	m := make(map[string]bool)
   107  	for _, k := range *t.filesystemEntries {
   108  		m[k] = true
   109  	}
   110  
   111  	// check for deleted/never created files
   112  	for k := range t.expected.Filesystem.Files {
   113  		_, ok := m[k]
   114  		if !ok {
   115  			v.AddInvalidReason(k, ReasonMissing)
   116  		}
   117  	}
   118  
   119  	for _, path := range *t.filesystemEntries {
   120  		if ShouldIgnore(path) {
   121  			continue
   122  		}
   123  
   124  		fileInfo, err := os.Lstat(path)
   125  		if err != nil {
   126  			return false
   127  		}
   128  
   129  		// check for newly added files
   130  		expectedFileInfo, ok := t.expected.Filesystem.Files[path]
   131  		if !ok {
   132  			v.AddInvalidReason(path, ReasonCreatedAfterBuild)
   133  			continue
   134  		}
   135  
   136  		// directories are not checked for size/hash
   137  		if fileInfo.IsDir() {
   138  			continue
   139  		}
   140  
   141  		// check file size
   142  		if fileInfo.Size() != expectedFileInfo.Size {
   143  			v.AddInvalidReason(path, ReasonSizeChanged)
   144  			boblog.Log.V(2).Info(fmt.Sprintf("failed to verify [%s], different sizes [current: %d != expected: %d]", path, fileInfo.Size(), expectedFileInfo.Size))
   145  		}
   146  
   147  		// Not comparing hashes of targets for now due to the performance penalty.
   148  		//
   149  		// // checks the contents hash of the file with the ones from build info
   150  		// hashOfFile, err := filehash.HashOfFile(path)
   151  		// if err != nil {
   152  		// 	return false
   153  		// }
   154  		// if hashOfFile != expectedFileInfo.Hash {
   155  		// 	v.AddInvalidReason(path, ReasonHashChanged)
   156  		// 	boblog.Log.V(2).Info(fmt.Sprintf("failed to verify [%s], different hashes [current: %s != expected: %s]", path, hashOfFile, expectedFileInfo.Hash))
   157  		// }
   158  	}
   159  
   160  	return len(v.InvalidFiles) == 0
   161  }
   162  
   163  var IgnoredTargets = []string{"node_modules/.cache"}
   164  
   165  // ShouldIgnore checks if file path should be ignored
   166  // when creating/extracting artifact or creating the buildinfo
   167  func ShouldIgnore(path string) bool {
   168  	for _, v := range IgnoredTargets {
   169  		if strings.Contains(path, v) {
   170  			return true
   171  		}
   172  	}
   173  	return false
   174  }
   175  
   176  func (t *T) verifyFilesystem() bool {
   177  	if !t.preConditionsFilesystem() {
   178  		return false
   179  	}
   180  
   181  	h := filehash.New()
   182  
   183  	for _, path := range *t.filesystemEntries {
   184  
   185  		fileInfo, err := os.Stat(path)
   186  		if err != nil {
   187  			return false
   188  		}
   189  
   190  		expectedFileInfo, ok := t.expected.Filesystem.Files[path]
   191  		if !ok {
   192  
   193  			return false
   194  		}
   195  
   196  		// Compare size of the target
   197  		if fileInfo.Size() != expectedFileInfo.Size {
   198  			return false
   199  		}
   200  
   201  		err = h.AddFile(path)
   202  		if err != nil {
   203  			return false
   204  		}
   205  	}
   206  
   207  	ret := hex.EncodeToString(h.Sum()) == t.expected.Filesystem.Hash
   208  
   209  	return ret
   210  }
   211  
   212  func (t *T) verifyDocker() bool {
   213  	if len(t.dockerImages) == 0 {
   214  		return true
   215  	}
   216  
   217  	// In case there was no previous local build
   218  	// verify return false indicating that there can't
   219  	// exist a valid target from a previous build.
   220  	// Loading from the cash must be handled by the calling function.
   221  	if t.expected == nil {
   222  		return false
   223  	}
   224  
   225  	// This usually indicates an image was added/removed manually
   226  	// from a target directory.
   227  	if len(t.expected.Docker) != len(t.dockerImages) {
   228  		return false
   229  	}
   230  
   231  	for _, image := range t.dockerImages {
   232  		expectedImageInfo, ok := t.expected.Docker[image]
   233  		if !ok {
   234  			return false
   235  		}
   236  
   237  		exists, err := t.dockerRegistryClient.ImageExists(image)
   238  		if err != nil {
   239  			return false
   240  		}
   241  		if !exists {
   242  			return false
   243  		}
   244  
   245  		imageHash, err := t.dockerImageHash(image)
   246  		if err != nil {
   247  			boblog.Log.Error(err, fmt.Sprintf("Unable to verify docker image hash [%s]", image))
   248  			return false
   249  		}
   250  		if imageHash != expectedImageInfo.Hash {
   251  			return false
   252  		}
   253  	}
   254  
   255  	return true
   256  }