zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/storage/scrub.go (about) 1 package storage 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "strings" 10 "time" 11 12 "github.com/olekukonko/tablewriter" 13 godigest "github.com/opencontainers/go-digest" 14 ispec "github.com/opencontainers/image-spec/specs-go/v1" 15 16 zerr "zotregistry.dev/zot/errors" 17 "zotregistry.dev/zot/pkg/common" 18 storageTypes "zotregistry.dev/zot/pkg/storage/types" 19 ) 20 21 const ( 22 colImageNameIndex = iota 23 colTagIndex 24 colStatusIndex 25 colAffectedBlobIndex 26 colErrorIndex 27 28 imageNameWidth = 32 29 tagWidth = 24 30 statusWidth = 8 31 affectedBlobWidth = 24 32 errorWidth = 8 33 ) 34 35 type ScrubImageResult struct { 36 ImageName string `json:"imageName"` 37 Tag string `json:"tag"` 38 Status string `json:"status"` 39 AffectedBlob string `json:"affectedBlob"` 40 Error string `json:"error"` 41 } 42 43 type ScrubResults struct { 44 ScrubResults []ScrubImageResult `json:"scrubResults"` 45 } 46 47 func (sc StoreController) CheckAllBlobsIntegrity(ctx context.Context) (ScrubResults, error) { 48 results := ScrubResults{} 49 50 imageStoreList := make(map[string]storageTypes.ImageStore) 51 if sc.SubStore != nil { 52 imageStoreList = sc.SubStore 53 } 54 55 imageStoreList[""] = sc.DefaultStore 56 57 for _, imgStore := range imageStoreList { 58 imgStoreResults, err := CheckImageStoreBlobsIntegrity(ctx, imgStore) 59 if err != nil { 60 return results, err 61 } 62 63 results.ScrubResults = append(results.ScrubResults, imgStoreResults...) 64 } 65 66 return results, nil 67 } 68 69 func CheckImageStoreBlobsIntegrity(ctx context.Context, imgStore storageTypes.ImageStore) ([]ScrubImageResult, error) { 70 results := []ScrubImageResult{} 71 72 repos, err := imgStore.GetRepositories() 73 if err != nil { 74 return results, err 75 } 76 77 for _, repo := range repos { 78 imageResults, err := CheckRepo(ctx, repo, imgStore) 79 if err != nil { 80 return results, err 81 } 82 83 results = append(results, imageResults...) 84 } 85 86 return results, nil 87 } 88 89 // CheckRepo is the main entry point for the scrub task 90 // We aim for eventual consistency (locks, etc) since this task contends with data path. 91 func CheckRepo(ctx context.Context, imageName string, imgStore storageTypes.ImageStore) ([]ScrubImageResult, error) { 92 results := []ScrubImageResult{} 93 94 // getIndex holds the lock 95 indexContent, err := getIndex(imageName, imgStore) 96 if err != nil { 97 return results, err 98 } 99 100 var index ispec.Index 101 if err := json.Unmarshal(indexContent, &index); err != nil { 102 return results, zerr.ErrRepoNotFound 103 } 104 105 scrubbedManifests := make(map[godigest.Digest]ScrubImageResult) 106 107 for _, manifest := range index.Manifests { 108 if common.IsContextDone(ctx) { 109 return results, ctx.Err() 110 } 111 112 tag := manifest.Annotations[ispec.AnnotationRefName] 113 114 // checkImage holds the lock 115 layers, err := checkImage(manifest, imgStore, imageName, tag, scrubbedManifests) 116 if err == nil && len(layers) > 0 { 117 // CheckLayers doesn't use locks 118 imgRes := CheckLayers(imageName, tag, layers, imgStore) 119 scrubbedManifests[manifest.Digest] = imgRes 120 } 121 122 // ignore the manifest if it isn't found 123 if !errors.Is(err, zerr.ErrManifestNotFound) { 124 results = append(results, scrubbedManifests[manifest.Digest]) 125 } 126 } 127 128 return results, nil 129 } 130 131 func checkImage( 132 manifest ispec.Descriptor, imgStore storageTypes.ImageStore, imageName, tag string, 133 scrubbedManifests map[godigest.Digest]ScrubImageResult, 134 ) ([]ispec.Descriptor, error) { 135 var lockLatency time.Time 136 137 imgStore.RLock(&lockLatency) 138 defer imgStore.RUnlock(&lockLatency) 139 140 manifestContent, err := imgStore.GetBlobContent(imageName, manifest.Digest) 141 if err != nil { 142 // ignore if the manifest is not found(probably it was deleted after we got the list of manifests) 143 return []ispec.Descriptor{}, zerr.ErrManifestNotFound 144 } 145 146 return scrubManifest(manifest, imgStore, imageName, tag, manifestContent, scrubbedManifests) 147 } 148 149 func getIndex(imageName string, imgStore storageTypes.ImageStore) ([]byte, error) { 150 var lockLatency time.Time 151 152 imgStore.RLock(&lockLatency) 153 defer imgStore.RUnlock(&lockLatency) 154 155 // check image structure / layout 156 ok, err := imgStore.ValidateRepo(imageName) 157 if err != nil { 158 return []byte{}, err 159 } 160 161 if !ok { 162 return []byte{}, zerr.ErrRepoBadLayout 163 } 164 165 // check "index.json" content 166 indexContent, err := imgStore.GetIndexContent(imageName) 167 if err != nil { 168 return []byte{}, err 169 } 170 171 return indexContent, nil 172 } 173 174 func scrubManifest( 175 manifest ispec.Descriptor, imgStore storageTypes.ImageStore, imageName, tag string, 176 manifestContent []byte, scrubbedManifests map[godigest.Digest]ScrubImageResult, 177 ) ([]ispec.Descriptor, error) { 178 layers := []ispec.Descriptor{} 179 180 res, ok := scrubbedManifests[manifest.Digest] 181 if ok { 182 scrubbedManifests[manifest.Digest] = newScrubImageResult(imageName, tag, res.Status, 183 res.AffectedBlob, res.Error) 184 185 return layers, nil 186 } 187 188 switch manifest.MediaType { 189 case ispec.MediaTypeImageIndex: 190 var idx ispec.Index 191 if err := json.Unmarshal(manifestContent, &idx); err != nil { 192 imgRes := getResult(imageName, tag, manifest.Digest, zerr.ErrBadBlobDigest) 193 scrubbedManifests[manifest.Digest] = imgRes 194 195 return layers, err 196 } 197 198 // check all manifests 199 for _, man := range idx.Manifests { 200 buf, err := imgStore.GetBlobContent(imageName, man.Digest) 201 if err != nil { 202 imgRes := getResult(imageName, tag, man.Digest, zerr.ErrBadBlobDigest) 203 scrubbedManifests[man.Digest] = imgRes 204 scrubbedManifests[manifest.Digest] = imgRes 205 206 return layers, err 207 } 208 209 layersToScrub, err := scrubManifest(man, imgStore, imageName, tag, buf, scrubbedManifests) 210 211 if err == nil { 212 layers = append(layers, layersToScrub...) 213 } 214 215 // if the manifest is affected then this index is also affected 216 if scrubbedManifests[man.Digest].Error != "" { 217 mRes := scrubbedManifests[man.Digest] 218 219 scrubbedManifests[manifest.Digest] = newScrubImageResult(imageName, tag, mRes.Status, 220 mRes.AffectedBlob, mRes.Error) 221 222 return layers, err 223 } 224 } 225 226 // at this point, before starting to check the subject we can consider the index is ok 227 scrubbedManifests[manifest.Digest] = getResult(imageName, tag, "", nil) 228 229 // check subject if exists 230 if idx.Subject != nil { 231 buf, err := imgStore.GetBlobContent(imageName, idx.Subject.Digest) 232 if err != nil { 233 imgRes := getResult(imageName, tag, idx.Subject.Digest, zerr.ErrBadBlobDigest) 234 scrubbedManifests[idx.Subject.Digest] = imgRes 235 scrubbedManifests[manifest.Digest] = imgRes 236 237 return layers, err 238 } 239 240 layersToScrub, err := scrubManifest(*idx.Subject, imgStore, imageName, tag, buf, scrubbedManifests) 241 242 if err == nil { 243 layers = append(layers, layersToScrub...) 244 } 245 246 subjectRes := scrubbedManifests[idx.Subject.Digest] 247 248 scrubbedManifests[manifest.Digest] = newScrubImageResult(imageName, tag, subjectRes.Status, 249 subjectRes.AffectedBlob, subjectRes.Error) 250 251 return layers, err 252 } 253 254 return layers, nil 255 case ispec.MediaTypeImageManifest: 256 affectedBlob, man, err := CheckManifestAndConfig(imageName, manifest, manifestContent, imgStore) 257 if err == nil { 258 layers = append(layers, man.Layers...) 259 } 260 261 scrubbedManifests[manifest.Digest] = getResult(imageName, tag, affectedBlob, err) 262 263 // if integrity ok then check subject if exists 264 if err == nil && man.Subject != nil { 265 buf, err := imgStore.GetBlobContent(imageName, man.Subject.Digest) 266 if err != nil { 267 imgRes := getResult(imageName, tag, man.Subject.Digest, zerr.ErrBadBlobDigest) 268 scrubbedManifests[man.Subject.Digest] = imgRes 269 scrubbedManifests[manifest.Digest] = imgRes 270 271 return layers, err 272 } 273 274 layersToScrub, err := scrubManifest(*man.Subject, imgStore, imageName, tag, buf, scrubbedManifests) 275 276 if err == nil { 277 layers = append(layers, layersToScrub...) 278 } 279 280 subjectRes := scrubbedManifests[man.Subject.Digest] 281 282 scrubbedManifests[manifest.Digest] = newScrubImageResult(imageName, tag, subjectRes.Status, 283 subjectRes.AffectedBlob, subjectRes.Error) 284 285 return layers, err 286 } 287 288 return layers, err 289 default: 290 scrubbedManifests[manifest.Digest] = getResult(imageName, tag, manifest.Digest, zerr.ErrBadManifest) 291 292 return layers, zerr.ErrBadManifest 293 } 294 } 295 296 func CheckManifestAndConfig( 297 imageName string, manifestDesc ispec.Descriptor, manifestContent []byte, imgStore storageTypes.ImageStore, 298 ) (godigest.Digest, ispec.Manifest, error) { 299 if manifestDesc.MediaType != ispec.MediaTypeImageManifest { 300 return manifestDesc.Digest, ispec.Manifest{}, zerr.ErrBadManifest 301 } 302 303 var manifest ispec.Manifest 304 305 err := json.Unmarshal(manifestContent, &manifest) 306 if err != nil { 307 return manifestDesc.Digest, ispec.Manifest{}, zerr.ErrBadManifest 308 } 309 310 configContent, err := imgStore.GetBlobContent(imageName, manifest.Config.Digest) 311 if err != nil { 312 return manifest.Config.Digest, ispec.Manifest{}, err 313 } 314 315 var config ispec.Image 316 317 err = json.Unmarshal(configContent, &config) 318 if err != nil { 319 return manifest.Config.Digest, ispec.Manifest{}, zerr.ErrBadConfig 320 } 321 322 return "", manifest, nil 323 } 324 325 func CheckLayers( 326 imageName, tagName string, layers []ispec.Descriptor, imgStore storageTypes.ImageStore, 327 ) ScrubImageResult { 328 imageRes := ScrubImageResult{} 329 330 for _, layer := range layers { 331 if err := imgStore.VerifyBlobDigestValue(imageName, layer.Digest); err != nil { 332 imageRes = getResult(imageName, tagName, layer.Digest, err) 333 334 break 335 } 336 337 imageRes = getResult(imageName, tagName, "", nil) 338 } 339 340 return imageRes 341 } 342 343 func getResult(imageName, tag string, affectedBlobDigest godigest.Digest, err error) ScrubImageResult { 344 if err != nil { 345 return newScrubImageResult(imageName, tag, "affected", affectedBlobDigest.Encoded(), err.Error()) 346 } 347 348 return newScrubImageResult(imageName, tag, "ok", "", "") 349 } 350 351 func newScrubImageResult(imageName, tag, status, affectedBlob, err string) ScrubImageResult { 352 return ScrubImageResult{ 353 ImageName: imageName, 354 Tag: tag, 355 Status: status, 356 AffectedBlob: affectedBlob, 357 Error: err, 358 } 359 } 360 361 func getScrubTableWriter(writer io.Writer) *tablewriter.Table { 362 table := tablewriter.NewWriter(writer) 363 364 table.SetAutoWrapText(false) 365 table.SetAutoFormatHeaders(true) 366 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 367 table.SetAlignment(tablewriter.ALIGN_LEFT) 368 table.SetCenterSeparator("") 369 table.SetColumnSeparator("") 370 table.SetRowSeparator("") 371 table.SetHeaderLine(false) 372 table.SetBorder(false) 373 table.SetTablePadding(" ") 374 table.SetNoWhiteSpace(true) 375 table.SetColMinWidth(colImageNameIndex, imageNameWidth) 376 table.SetColMinWidth(colTagIndex, tagWidth) 377 table.SetColMinWidth(colStatusIndex, statusWidth) 378 table.SetColMinWidth(colErrorIndex, affectedBlobWidth) 379 table.SetColMinWidth(colErrorIndex, errorWidth) 380 381 return table 382 } 383 384 const tableCols = 5 385 386 func printScrubTableHeader(writer io.Writer) { 387 table := getScrubTableWriter(writer) 388 389 row := make([]string, tableCols) 390 391 row[colImageNameIndex] = "REPOSITORY" 392 row[colTagIndex] = "TAG" 393 row[colStatusIndex] = "STATUS" 394 row[colAffectedBlobIndex] = "AFFECTED BLOB" 395 row[colErrorIndex] = "ERROR" 396 397 table.Append(row) 398 table.Render() 399 } 400 401 func printImageResult(imageResult ScrubImageResult) string { 402 var builder strings.Builder 403 404 table := getScrubTableWriter(&builder) 405 table.SetColMinWidth(colImageNameIndex, imageNameWidth) 406 table.SetColMinWidth(colTagIndex, tagWidth) 407 table.SetColMinWidth(colStatusIndex, statusWidth) 408 table.SetColMinWidth(colAffectedBlobIndex, affectedBlobWidth) 409 table.SetColMinWidth(colErrorIndex, errorWidth) 410 411 row := make([]string, tableCols) 412 413 row[colImageNameIndex] = imageResult.ImageName 414 row[colTagIndex] = imageResult.Tag 415 row[colStatusIndex] = imageResult.Status 416 row[colAffectedBlobIndex] = imageResult.AffectedBlob 417 row[colErrorIndex] = imageResult.Error 418 419 table.Append(row) 420 table.Render() 421 422 return builder.String() 423 } 424 425 func (results ScrubResults) PrintScrubResults(resultWriter io.Writer) { 426 var builder strings.Builder 427 428 printScrubTableHeader(&builder) 429 fmt.Fprint(resultWriter, builder.String()) 430 431 for _, res := range results.ScrubResults { 432 imageResult := printImageResult(res) 433 fmt.Fprint(resultWriter, imageResult) 434 } 435 }