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  }