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 }