2021-10-05 04:12:22 -05:00
|
|
|
package storage
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strings"
|
2021-12-21 08:19:40 -05:00
|
|
|
"time"
|
2021-10-05 04:12:22 -05:00
|
|
|
|
|
|
|
"github.com/olekukonko/tablewriter"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
"github.com/opencontainers/umoci"
|
|
|
|
"github.com/opencontainers/umoci/oci/casext"
|
2022-10-20 11:39:20 -05:00
|
|
|
|
2021-10-05 04:12:22 -05:00
|
|
|
"zotregistry.io/zot/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
colImageNameIndex = iota
|
|
|
|
colTagIndex
|
|
|
|
colStatusIndex
|
|
|
|
colErrorIndex
|
|
|
|
|
|
|
|
imageNameWidth = 32
|
|
|
|
tagWidth = 24
|
|
|
|
statusWidth = 8
|
|
|
|
errorWidth = 8
|
|
|
|
)
|
|
|
|
|
|
|
|
type ScrubImageResult struct {
|
2021-12-13 14:23:31 -05:00
|
|
|
ImageName string `json:"imageName"`
|
2021-10-05 04:12:22 -05:00
|
|
|
Tag string `json:"tag"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
Error string `json:"error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ScrubResults struct {
|
2021-12-13 14:23:31 -05:00
|
|
|
ScrubResults []ScrubImageResult `json:"scrubResults"`
|
2021-10-05 04:12:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (sc StoreController) CheckAllBlobsIntegrity() (ScrubResults, error) {
|
|
|
|
results := ScrubResults{}
|
|
|
|
|
|
|
|
imageStoreList := make(map[string]ImageStore)
|
|
|
|
if sc.SubStore != nil {
|
|
|
|
imageStoreList = sc.SubStore
|
|
|
|
}
|
|
|
|
|
|
|
|
imageStoreList[""] = sc.DefaultStore
|
|
|
|
|
2021-12-13 14:23:31 -05:00
|
|
|
for _, imgStore := range imageStoreList {
|
2022-03-04 02:37:06 -05:00
|
|
|
imgStoreResults, err := CheckImageStoreBlobsIntegrity(imgStore)
|
2021-10-05 04:12:22 -05:00
|
|
|
if err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
2022-03-04 02:37:06 -05:00
|
|
|
results.ScrubResults = append(results.ScrubResults, imgStoreResults...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func CheckImageStoreBlobsIntegrity(imgStore ImageStore) ([]ScrubImageResult, error) {
|
|
|
|
results := []ScrubImageResult{}
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-03-04 02:37:06 -05:00
|
|
|
repos, err := imgStore.GetRepositories()
|
|
|
|
if err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, repo := range repos {
|
2022-05-09 17:30:11 -05:00
|
|
|
imageResults, err := CheckRepo(repo, imgStore)
|
2022-03-04 02:37:06 -05:00
|
|
|
if err != nil {
|
|
|
|
return results, err
|
2021-10-05 04:12:22 -05:00
|
|
|
}
|
2022-03-04 02:37:06 -05:00
|
|
|
|
|
|
|
results = append(results, imageResults...)
|
2021-10-05 04:12:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
2022-05-09 17:30:11 -05:00
|
|
|
func CheckRepo(imageName string, imgStore ImageStore) ([]ScrubImageResult, error) {
|
2021-10-05 04:12:22 -05:00
|
|
|
results := []ScrubImageResult{}
|
|
|
|
|
2021-12-13 14:23:31 -05:00
|
|
|
dir := path.Join(imgStore.RootDir(), imageName)
|
|
|
|
if !imgStore.DirExists(dir) {
|
2021-10-05 04:12:22 -05:00
|
|
|
return results, errors.ErrRepoNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
ctxUmoci := context.Background()
|
|
|
|
|
|
|
|
oci, err := umoci.OpenLayout(dir)
|
|
|
|
if err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
defer oci.Close()
|
|
|
|
|
2021-12-21 08:19:40 -05:00
|
|
|
var lockLatency time.Time
|
|
|
|
|
|
|
|
imgStore.RLock(&lockLatency)
|
|
|
|
defer imgStore.RUnlock(&lockLatency)
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-09-02 07:56:02 -05:00
|
|
|
buf, err := os.ReadFile(path.Join(dir, "index.json"))
|
2021-10-05 04:12:22 -05:00
|
|
|
if err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var index ispec.Index
|
|
|
|
if err := json.Unmarshal(buf, &index); err != nil {
|
|
|
|
return results, errors.ErrRepoNotFound
|
|
|
|
}
|
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
listOfManifests := []ispec.Descriptor{}
|
|
|
|
|
|
|
|
for _, manifest := range index.Manifests {
|
|
|
|
if manifest.MediaType == ispec.MediaTypeImageIndex {
|
|
|
|
buf, err := os.ReadFile(path.Join(dir, "blobs", manifest.Digest.Algorithm().String(), manifest.Digest.Encoded()))
|
|
|
|
if err != nil {
|
|
|
|
tagName := manifest.Annotations[ispec.AnnotationRefName]
|
|
|
|
imgRes := getResult(imageName, tagName, errors.ErrBadBlobDigest)
|
|
|
|
results = append(results, imgRes)
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var idx ispec.Index
|
|
|
|
if err := json.Unmarshal(buf, &idx); err != nil {
|
|
|
|
tagName := manifest.Annotations[ispec.AnnotationRefName]
|
|
|
|
imgRes := getResult(imageName, tagName, errors.ErrBadBlobDigest)
|
|
|
|
results = append(results, imgRes)
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
listOfManifests = append(listOfManifests, idx.Manifests...)
|
|
|
|
} else if manifest.MediaType == ispec.MediaTypeImageManifest {
|
|
|
|
listOfManifests = append(listOfManifests, manifest)
|
2021-10-05 04:12:22 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
for _, m := range listOfManifests {
|
|
|
|
tag := m.Annotations[ispec.AnnotationRefName]
|
|
|
|
imageResult := CheckIntegrity(ctxUmoci, imageName, tag, oci, m, dir)
|
|
|
|
results = append(results, imageResult)
|
|
|
|
}
|
|
|
|
|
2021-10-05 04:12:22 -05:00
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
func CheckIntegrity(ctx context.Context, imageName, tagName string, oci casext.Engine, manifest ispec.Descriptor, dir string) ScrubImageResult { //nolint: lll
|
2021-10-05 04:12:22 -05:00
|
|
|
// check manifest and config
|
2022-12-10 19:38:01 -05:00
|
|
|
if _, err := umoci.Stat(ctx, oci, manifest); err != nil {
|
|
|
|
return getResult(imageName, tagName, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check layers
|
|
|
|
return CheckLayers(imageName, tagName, dir, manifest)
|
|
|
|
}
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
func CheckLayers(imageName, tagName, dir string, manifest ispec.Descriptor) ScrubImageResult {
|
2021-10-05 04:12:22 -05:00
|
|
|
imageRes := ScrubImageResult{}
|
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
buf, err := os.ReadFile(path.Join(dir, "blobs", manifest.Digest.Algorithm().String(), manifest.Digest.Encoded()))
|
2021-10-05 04:12:22 -05:00
|
|
|
if err != nil {
|
|
|
|
imageRes = getResult(imageName, tagName, err)
|
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
return imageRes
|
|
|
|
}
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
var man ispec.Manifest
|
|
|
|
if err := json.Unmarshal(buf, &man); err != nil {
|
|
|
|
imageRes = getResult(imageName, tagName, err)
|
2021-12-13 14:23:31 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
return imageRes
|
|
|
|
}
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
for _, layer := range man.Layers {
|
|
|
|
layerPath := path.Join(dir, "blobs", layer.Digest.Algorithm().String(), layer.Digest.Encoded())
|
2021-12-13 14:23:31 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
_, err = os.Stat(layerPath)
|
|
|
|
if err != nil {
|
|
|
|
imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound)
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
break
|
|
|
|
}
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
layerFh, err := os.Open(layerPath)
|
|
|
|
if err != nil {
|
|
|
|
imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound)
|
2021-12-13 14:23:31 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
break
|
|
|
|
}
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
computedDigest, err := godigest.FromReader(layerFh)
|
|
|
|
layerFh.Close()
|
2021-12-13 14:23:31 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
if err != nil {
|
|
|
|
imageRes = getResult(imageName, tagName, errors.ErrBadBlobDigest)
|
2021-10-05 04:12:22 -05:00
|
|
|
|
2022-12-10 19:38:01 -05:00
|
|
|
break
|
2021-10-05 04:12:22 -05:00
|
|
|
}
|
2022-12-10 19:38:01 -05:00
|
|
|
|
|
|
|
if computedDigest != layer.Digest {
|
|
|
|
imageRes = getResult(imageName, tagName, errors.ErrBadBlobDigest)
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
imageRes = getResult(imageName, tagName, nil)
|
2021-10-05 04:12:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return imageRes
|
|
|
|
}
|
|
|
|
|
|
|
|
func getResult(imageName, tag string, err error) ScrubImageResult {
|
|
|
|
var status string
|
|
|
|
|
|
|
|
var errField string
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
status = "affected"
|
|
|
|
errField = err.Error()
|
|
|
|
} else {
|
|
|
|
status = "ok"
|
|
|
|
errField = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return ScrubImageResult{
|
|
|
|
ImageName: imageName,
|
|
|
|
Tag: tag,
|
|
|
|
Status: status,
|
|
|
|
Error: errField,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getScrubTableWriter(writer io.Writer) *tablewriter.Table {
|
|
|
|
table := tablewriter.NewWriter(writer)
|
|
|
|
|
|
|
|
table.SetAutoWrapText(false)
|
|
|
|
table.SetAutoFormatHeaders(true)
|
|
|
|
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
|
|
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
|
|
|
table.SetCenterSeparator("")
|
|
|
|
table.SetColumnSeparator("")
|
|
|
|
table.SetRowSeparator("")
|
|
|
|
table.SetHeaderLine(false)
|
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetTablePadding(" ")
|
|
|
|
table.SetNoWhiteSpace(true)
|
|
|
|
table.SetColMinWidth(colImageNameIndex, imageNameWidth)
|
|
|
|
table.SetColMinWidth(colTagIndex, tagWidth)
|
|
|
|
table.SetColMinWidth(colStatusIndex, statusWidth)
|
|
|
|
table.SetColMinWidth(colErrorIndex, errorWidth)
|
|
|
|
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
2021-12-13 14:23:31 -05:00
|
|
|
const tableCols = 4
|
|
|
|
|
2021-10-05 04:12:22 -05:00
|
|
|
func printScrubTableHeader(writer io.Writer) {
|
|
|
|
table := getScrubTableWriter(writer)
|
|
|
|
|
2021-12-13 14:23:31 -05:00
|
|
|
row := make([]string, tableCols)
|
2021-10-05 04:12:22 -05:00
|
|
|
|
|
|
|
row[colImageNameIndex] = "IMAGE NAME"
|
|
|
|
row[colTagIndex] = "TAG"
|
|
|
|
row[colStatusIndex] = "STATUS"
|
|
|
|
row[colErrorIndex] = "ERROR"
|
|
|
|
|
|
|
|
table.Append(row)
|
|
|
|
table.Render()
|
|
|
|
}
|
|
|
|
|
|
|
|
func printImageResult(imageResult ScrubImageResult) string {
|
|
|
|
var builder strings.Builder
|
|
|
|
|
|
|
|
table := getScrubTableWriter(&builder)
|
|
|
|
table.SetColMinWidth(colImageNameIndex, imageNameWidth)
|
|
|
|
table.SetColMinWidth(colTagIndex, tagWidth)
|
|
|
|
table.SetColMinWidth(colStatusIndex, statusWidth)
|
|
|
|
table.SetColMinWidth(colErrorIndex, errorWidth)
|
|
|
|
|
2021-12-13 14:23:31 -05:00
|
|
|
row := make([]string, tableCols)
|
2021-10-05 04:12:22 -05:00
|
|
|
|
|
|
|
row[colImageNameIndex] = imageResult.ImageName
|
|
|
|
row[colTagIndex] = imageResult.Tag
|
|
|
|
row[colStatusIndex] = imageResult.Status
|
|
|
|
row[colErrorIndex] = imageResult.Error
|
|
|
|
|
|
|
|
table.Append(row)
|
|
|
|
table.Render()
|
|
|
|
|
|
|
|
return builder.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (results ScrubResults) PrintScrubResults(resultWriter io.Writer) {
|
|
|
|
var builder strings.Builder
|
|
|
|
|
|
|
|
printScrubTableHeader(&builder)
|
|
|
|
fmt.Fprint(resultWriter, builder.String())
|
|
|
|
|
|
|
|
for _, res := range results.ScrubResults {
|
|
|
|
imageResult := printImageResult(res)
|
|
|
|
fmt.Fprint(resultWriter, imageResult)
|
|
|
|
}
|
|
|
|
}
|