Files
zot/pkg/storage/gcs/gcs_test.go
T
Andrei Aaron a5cc8ab810 feat: support pushing multiple tags for a single manifest (#3885)
* feat: support pushing multiple tags for a single manifest

See https://github.com/opencontainers/distribution-spec/pull/600

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

* fix: constants not replaced in swagger output

Also godot mandates comments ending in dots,
which produces bad results in the swagger generated files, see the extra ". which is now fixed below:

```
diff --git a/swagger/docs.go b/swagger/docs.go
index 84b08277..fb2c45c3 100644
--- a/swagger/docs.go
+++ b/swagger/docs.go
@@ -114,7 +114,7 @@ const docTemplate = `{
                         }
                     },
                     "400": {
-                        "description": "bad request\".",
+                        "description": "bad request",
                         "schema": {
                             "type": "string"
                         }
@@ -200,7 +200,7 @@ const docTemplate = `{
                         }
                     },
                     "400": {
-                        "description": "bad request\".",
+                        "description": "bad request",
                         "schema": {
                             "type": "string"
                         }
diff --git a/swagger/swagger.json b/swagger/swagger.json
index cfeb3900..247f95fa 100644
--- a/swagger/swagger.json
+++ b/swagger/swagger.json
@@ -106,7 +106,7 @@
                         }
                     },
                     "400": {
-                        "description": "bad request\".",
+                        "description": "bad request",
                         "schema": {
                             "type": "string"
                         }
@@ -192,7 +192,7 @@
                         }
                     },
                     "400": {
-                        "description": "bad request\".",
+                        "description": "bad request",
                         "schema": {
                             "type": "string"
                         }
diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml
index 57641c2f..09b30dcc 100644
--- a/swagger/swagger.yaml
+++ b/swagger/swagger.yaml
@@ -310,7 +310,7 @@ paths:
           schema:
             type: string
         "400":
-          description: bad request".
+          description: bad request
           schema:
             type: string
         "500":
@@ -366,7 +366,7 @@ paths:
           schema:
             type: string
         "400":
-          description: bad request".
+          description: bad request
           schema:
             type: string
         "500":
```

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

---------

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2026-03-29 21:13:24 +03:00

3683 lines
111 KiB
Go

//go:build needprivileges && linux
package gcs_test
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"os"
"os/exec"
"path"
"regexp"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/distribution/v3/registry/storage/driver/factory"
guuid "github.com/gofrs/uuid"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/api/config"
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
"zotregistry.dev/zot/v2/pkg/log"
"zotregistry.dev/zot/v2/pkg/storage"
"zotregistry.dev/zot/v2/pkg/storage/cache"
common "zotregistry.dev/zot/v2/pkg/storage/common"
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
"zotregistry.dev/zot/v2/pkg/storage/gc"
"zotregistry.dev/zot/v2/pkg/storage/gcs"
storageTypes "zotregistry.dev/zot/v2/pkg/storage/types"
. "zotregistry.dev/zot/v2/pkg/test/image-utils"
"zotregistry.dev/zot/v2/pkg/test/mocks"
tskip "zotregistry.dev/zot/v2/pkg/test/skip"
)
//nolint:gochecknoglobals // test constants
const (
repoName = "test"
tag = "0.0.1"
)
var (
trueVal bool = true //nolint: gochecknoglobals
errGCSMockEndpointNotSet = errors.New("GCSMOCK_ENDPOINT must be set for GCS tests")
errUnexpectedError = errors.New("unexpected err")
errBucketCreateFailed = errors.New("failed to create bucket")
)
// httpsProxyServer manages an HTTPS proxy server on port 443.
type httpsProxyServer struct {
server *http.Server
listener net.Listener
wg sync.WaitGroup
target string
certFile string // Path to the certificate file for cleanup
}
// newHTTPSProxyServer creates a new HTTPS proxy server that forwards requests to the target.
func newHTTPSProxyServer(target string) (*httpsProxyServer, error) {
// Generate self-signed certificate
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %w", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "oauth2.googleapis.com",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"oauth2.googleapis.com", "www.googleapis.com", "storage.googleapis.com"},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, fmt.Errorf("failed to create key pair: %w", err)
}
// Write certificate to a temporary file so we can add it to the trusted certificates
// via SSL_CERT_FILE environment variable. This is the standard way to add custom
// trusted certificates and works with Go's crypto/x509 package, including OAuth2 clients.
certFile, err := os.CreateTemp("", "gcs-test-cert-*.pem")
if err != nil {
return nil, fmt.Errorf("failed to create temp cert file: %w", err)
}
if _, err := certFile.Write(certPEM); err != nil {
certFile.Close()
os.Remove(certFile.Name())
return nil, fmt.Errorf("failed to write cert to file: %w", err)
}
if err := certFile.Close(); err != nil {
os.Remove(certFile.Name())
return nil, fmt.Errorf("failed to close cert file: %w", err)
}
// Create proxy handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Build target URL
targetURL := target + r.URL.Path
if r.URL.RawQuery != "" {
targetURL += "?" + r.URL.RawQuery
}
// Create request to target
req, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Copy headers
for key, values := range r.Header {
if key != "Host" && key != "Connection" {
for _, value := range values {
req.Header.Add(key, value)
}
}
}
// Make request
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// Copy response headers
for key, values := range resp.Header {
if key != "Connection" && key != "Transfer-Encoding" {
for _, value := range values {
w.Header().Add(key, value)
}
}
}
// Copy status and body
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
})
// Create HTTP server with TLS config (test-only proxy).
server := &http.Server{
Handler: handler,
ReadHeaderTimeout: 10 * time.Second,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
},
}
// Try to listen on port 443 (requires root or CAP_NET_BIND_SERVICE for tests).
lc := net.ListenConfig{}
listener, err := lc.Listen(context.Background(), "tcp", ":443") //nolint:gosec // G102: test proxy must listen on 443
if err != nil {
return nil, fmt.Errorf("failed to listen on port 443: %w (may require root or CAP_NET_BIND_SERVICE)", err)
}
tlsListener := tls.NewListener(listener, server.TLSConfig)
return &httpsProxyServer{
server: server,
listener: tlsListener,
target: target,
certFile: certFile.Name(),
}, nil
}
func (p *httpsProxyServer) Start() {
p.wg.Add(1) //nolint:modernize // standard sync.WaitGroup usage
go func() {
defer p.wg.Done()
_ = p.server.Serve(p.listener)
}()
}
func (p *httpsProxyServer) Stop() {
_ = p.listener.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = p.server.Shutdown(ctx)
p.wg.Wait()
}
var httpsProxy *httpsProxyServer //nolint:gochecknoglobals // Test fixture shared by TestMain.
// setupHostsEntries adds entries to /etc/hosts to redirect Google API domains to localhost.
func setupHostsEntries() error {
entries := []string{
"127.0.0.1 www.googleapis.com",
"127.0.0.1 storage.googleapis.com",
"127.0.0.1 oauth2.googleapis.com",
}
for _, entry := range entries {
// Check if entry already exists.
//nolint:gosec // G204: test-only, fixed entries
cmd := exec.CommandContext(context.Background(), "grep", "-q", strings.Fields(entry)[1], "/etc/hosts")
if cmd.Run() == nil {
// Entry already exists, skip
continue
}
// Add entry (requires privileges).
//nolint:gosec // G204: test-only, controlled entry
cmd = exec.CommandContext(context.Background(), "sh", "-c", fmt.Sprintf("echo '%s' >> /etc/hosts", entry))
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to add %s to /etc/hosts: %w", entry, err)
}
}
return nil
}
// teardownHostsEntries removes entries from /etc/hosts that were added for the emulator.
func teardownHostsEntries() {
domains := []string{
"www.googleapis.com",
"storage.googleapis.com",
"oauth2.googleapis.com",
}
for _, domain := range domains {
// Remove entry (requires privileges).
//nolint:gosec // G204: test-only, fixed domains
pattern := fmt.Sprintf("/%s/d", strings.ReplaceAll(domain, ".", "\\."))
cmd := exec.CommandContext(context.Background(), "sed", "-i", pattern, "/etc/hosts")
_ = cmd.Run() // Ignore errors - entry might not exist
}
}
// TestMain sets up and tears down the HTTPS proxy and /etc/hosts entries for all tests in this package.
// TestMain runs once before all tests and once after all tests complete.
// It applies to all test files in the same package (gcs_test package).
func TestMain(m *testing.M) {
// Setup /etc/hosts entries if GCSMOCK_ENDPOINT is set
if os.Getenv("GCSMOCK_ENDPOINT") != "" {
if err := setupHostsEntries(); err != nil {
fmt.Printf("Warning: Could not modify /etc/hosts: %v\n", err)
fmt.Printf("Tests may fail if /etc/hosts entries are not present\n")
} else {
fmt.Println("Added /etc/hosts entries for Google API domains")
}
}
// Start HTTPS proxy before all tests if GCSMOCK_ENDPOINT is set
if os.Getenv("GCSMOCK_ENDPOINT") != "" {
endpoint := os.Getenv("GCSMOCK_ENDPOINT")
endpoint = strings.TrimSuffix(endpoint, "/")
target := endpoint
var err error
httpsProxy, err = newHTTPSProxyServer(target)
if err != nil {
// Fail fast: with /etc/hosts redirecting Google domains to 127.0.0.1,
// OAuth/token calls will hit localhost:443 and fail with unclear errors
// if the proxy is not listening. Require the proxy to start.
fmt.Fprintf(os.Stderr, "Fatal: cannot start HTTPS proxy on port 443: %v\n", err)
fmt.Fprintf(os.Stderr, "This may require root or CAP_NET_BIND_SERVICE. Exiting.\n")
os.Exit(1)
}
httpsProxy.Start()
// Set SSL_CERT_FILE to trust our self-signed certificate
// This is respected by Go's crypto/x509 package when loading the system cert pool
// and will affect all TLS connections, including those made by OAuth2 clients
os.Setenv("SSL_CERT_FILE", httpsProxy.certFile)
fmt.Printf("HTTPS proxy started on port 443, certificate: %s\n", httpsProxy.certFile)
}
// Run all tests
code := m.Run()
// Stop proxy after all tests finish
if httpsProxy != nil {
httpsProxy.Stop()
fmt.Println("HTTPS proxy stopped")
httpsProxy = nil
}
// Cleanup /etc/hosts entries
if os.Getenv("GCSMOCK_ENDPOINT") != "" {
teardownHostsEntries()
fmt.Println("Removed /etc/hosts entries for Google API domains")
}
os.Exit(code)
}
func ensureDummyGCSCreds(t *testing.T) {
t.Helper()
if os.Getenv("GCSMOCK_ENDPOINT") != "" {
credsFile := path.Join(t.TempDir(), "dummy_creds.json")
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
t.Fatal(err)
}
privPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privBytes,
})
content := fmt.Sprintf(`{"type": "service_account", "project_id": "test-project", `+
`"client_email": "test@test.com", "private_key": %q}`, string(privPEM))
err = os.WriteFile(credsFile, []byte(content), 0o600)
if err != nil {
t.Fatal(err)
}
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", credsFile)
}
}
func cleanupStorage(store driver.StorageDriver, name string) {
_ = store.Delete(context.Background(), name)
}
// createObjectsStore creates a GCS-backed store; dedupe is always true at call sites.
//
//nolint:unparam
func createObjectsStore(rootDir string, cacheDir string, dedupe bool) (
driver.StorageDriver,
storageTypes.ImageStore,
error,
) {
bucket := "zot-storage-test"
endpoint := os.Getenv("GCSMOCK_ENDPOINT")
if endpoint == "" {
return nil, nil, errGCSMockEndpointNotSet
}
url := strings.TrimSuffix(endpoint, "/") + "/storage/v1/b?project=test-project"
body := fmt.Sprintf(`{"name": "%s"}`, bucket)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(body))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) //nolint:gosec // G107: Test mock
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
// Check if bucket was created successfully or already exists.
okStatus := resp.StatusCode == http.StatusOK ||
resp.StatusCode == http.StatusCreated ||
resp.StatusCode == http.StatusConflict
if !okStatus {
respBody, _ := io.ReadAll(resp.Body)
return nil, nil, fmt.Errorf("%w %s: status %d body %s",
errBucketCreateFailed, bucket, resp.StatusCode, string(respBody))
}
storageDriverParams := map[string]any{
"rootDir": rootDir,
"name": "gcs",
"bucket": bucket,
}
storeName := fmt.Sprintf("%v", storageDriverParams["name"])
store, err := factory.Create(context.Background(), storeName, storageDriverParams)
if err != nil {
return nil, nil, err
}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
var cacheDriver storageTypes.Cache
// from pkg/cli/server/root.go/applyDefaultValues, s3 magic
s3CacheDBPath := path.Join(cacheDir, storageConstants.BoltdbName+storageConstants.DBExtensionName)
if _, err := os.Stat(s3CacheDBPath); dedupe || (!dedupe && err == nil) {
cacheDriver, _ = storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: cacheDir,
Name: "cache",
UseRelPaths: false,
}, log)
}
il := gcs.NewImageStore(rootDir, cacheDir, dedupe, false, log, metrics, nil, store, cacheDriver, nil, nil)
return store, il, nil
}
func TestGCSDriver(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
Convey("GCS Driver E2E", t, func() {
// Create a fresh temp dir for each run to avoid BoltDB lock issues
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
So(err, ShouldBeNil)
defer cleanupStorage(storeDriver, testDir)
Convey("Init Repo", func() {
repoName := "test-repo-init"
err := imgStore.InitRepo(repoName)
So(err, ShouldBeNil)
isValid, err := imgStore.ValidateRepo(repoName)
So(err, ShouldBeNil)
So(isValid, ShouldBeTrue)
})
Convey("Push and Pull Image", func() {
repoName := "test-repo-push"
image := CreateDefaultImage()
// Upload layers
for _, content := range image.Layers {
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, digest)
So(err, ShouldBeNil)
}
// Upload config
cblob, err := json.Marshal(image.Config)
So(err, ShouldBeNil)
cdigest := godigest.FromBytes(cblob)
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewBuffer(cblob), cdigest)
So(err, ShouldBeNil)
// Upload manifest
mblob, err := json.Marshal(image.Manifest)
So(err, ShouldBeNil)
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob, nil)
So(err, ShouldBeNil)
// Verify manifest
_, _, _, err = imgStore.GetImageManifest(repoName, "1.0")
So(err, ShouldBeNil)
// Verify blob
blobReadCloser, _, err := imgStore.GetBlob(repoName, cdigest, ispec.MediaTypeImageConfig)
So(err, ShouldBeNil)
defer blobReadCloser.Close()
content, err := io.ReadAll(blobReadCloser)
So(err, ShouldBeNil)
So(content, ShouldResemble, cblob)
})
Convey("Delete Image", func() {
repoName := "test-repo-delete"
// Setup image
image := CreateDefaultImage()
// Upload layers first (required for manifest validation)
for _, content := range image.Layers {
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, digest)
So(err, ShouldBeNil)
}
// Upload config
cblob, err := json.Marshal(image.Config)
So(err, ShouldBeNil)
cdigest := godigest.FromBytes(cblob)
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewBuffer(cblob), cdigest)
So(err, ShouldBeNil)
// Upload manifest
mblob, err := json.Marshal(image.Manifest)
So(err, ShouldBeNil)
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob, nil)
So(err, ShouldBeNil)
err = imgStore.DeleteImageManifest(repoName, "1.0", false)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, "1.0")
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue)
})
})
}
func TestGCSDedupe(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
Convey("Dedupe", t, func(c C) {
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
So(err, ShouldBeNil)
defer cleanupStorage(storeDriver, testDir)
// manifest1
upload, err := imgStore.NewBlobUpload("dedupe1")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data3")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed("dedupe1", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest1 := digest
So(blobDigest1, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe1", upload, buf, digest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
ok, checkBlobSize1, err := imgStore.CheckBlob("dedupe1", digest)
So(ok, ShouldBeTrue)
So(checkBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
ok, checkBlobSize1, _, err = imgStore.StatBlob("dedupe1", digest)
So(ok, ShouldBeTrue)
So(checkBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
blobReadCloser, getBlobSize1, err := imgStore.GetBlob("dedupe1", digest,
"application/vnd.oci.image.layer.v1.tar+gzip")
So(getBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
err = blobReadCloser.Close()
So(err, ShouldBeNil)
cblob, cdigest := GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload("dedupe1", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob("dedupe1", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBuf)
_, _, err = imgStore.PutImageManifest("dedupe1", manifestDigest.String(),
ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe1", manifestDigest.String())
So(err, ShouldBeNil)
// manifest2
upload, err = imgStore.NewBlobUpload("dedupe2")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content = []byte("test-data3")
buf = bytes.NewBuffer(content)
buflen = buf.Len()
digest = godigest.FromBytes(content)
blob, err = imgStore.PutBlobChunkStreamed("dedupe2", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest2 := digest
So(blobDigest2, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe2", upload, buf, digest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
ok, checkBlobSize2, err := imgStore.CheckBlob("dedupe2", digest)
So(ok, ShouldBeTrue)
So(checkBlobSize2, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
ok, checkBlobSize2, _, err = imgStore.StatBlob("dedupe2", digest)
So(ok, ShouldBeTrue)
So(checkBlobSize2, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
blobReadCloser, getBlobSize2, err := imgStore.GetBlob("dedupe2", digest,
"application/vnd.oci.image.layer.v1.tar+gzip")
So(getBlobSize2, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
err = blobReadCloser.Close()
So(err, ShouldBeNil)
cblob, cdigest = GetRandomImageConfig()
_, clen, err = imgStore.FullBlobUpload("dedupe2", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err = imgStore.CheckBlob("dedupe2", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest = ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
manifestDigest = godigest.FromBytes(manifestBuf)
_, _, err = imgStore.PutImageManifest("dedupe2", manifestDigest.String(),
ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe2", manifestDigest.String())
So(err, ShouldBeNil)
So(blobDigest1, ShouldEqual, blobDigest2)
So(checkBlobSize1, ShouldEqual, checkBlobSize2)
So(getBlobSize1, ShouldEqual, getBlobSize2)
})
}
func TestGCSPullRange(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
Convey("Pull range", t, func(c C) {
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
So(err, ShouldBeNil)
defer cleanupStorage(storeDriver, testDir)
upload, err := imgStore.NewBlobUpload("test")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data3")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed("test", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload("test", upload, buf, digest)
So(err, ShouldBeNil)
blobReadCloser, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
err = blobReadCloser.Close()
So(err, ShouldBeNil)
// get range
blobReadCloser, _, _, err = imgStore.GetBlobPartial("test", digest,
"application/vnd.oci.image.layer.v1.tar+gzip", 0, 4)
So(err, ShouldBeNil)
buf.Reset()
_, err = buf.ReadFrom(blobReadCloser)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, "test-")
err = blobReadCloser.Close()
So(err, ShouldBeNil)
// get range - "data3" is bytes 5-9 (inclusive) of "test-data3"
blobReadCloser, _, _, err = imgStore.GetBlobPartial("test", digest,
"application/vnd.oci.image.layer.v1.tar+gzip", 5, 9)
So(err, ShouldBeNil)
buf.Reset()
_, err = buf.ReadFrom(blobReadCloser)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, "data3")
err = blobReadCloser.Close()
So(err, ShouldBeNil)
// get range from negative offset
blobReadCloser, _, _, err = imgStore.GetBlobPartial("test", digest,
"application/vnd.oci.image.layer.v1.tar+gzip", -4, 4)
So(err, ShouldNotBeNil)
So(blobReadCloser, ShouldBeNil)
})
}
func TestGCSGetAllDedupeReposCandidates(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(storeDriver, testDir)
Convey("Push repos with deduped blobs", t, func(c C) {
repoNames := []string{
"first",
"second",
"repo/a",
"repo/a/b/c/d/e/f",
"repo/repo-b/blobs",
"foo/bar/baz",
"blobs/foo/bar/blobs",
"blobs",
"blobs/foo",
}
storeController := storage.StoreController{DefaultStore: imgStore}
image := CreateRandomImage()
for _, repoName := range repoNames {
err := WriteImageToFileSystem(image, repoName, tag, storeController)
So(err, ShouldBeNil)
}
randomBlobDigest := image.Manifest.Layers[0].Digest
repos, err := imgStore.GetAllDedupeReposCandidates(randomBlobDigest)
So(err, ShouldBeNil)
slices.Sort(repoNames)
slices.Sort(repos)
So(repoNames, ShouldResemble, repos)
})
}
func TestGCSDeleteBlobsInUse(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
Convey("Setup manifest", t, func() {
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
So(err, ShouldBeNil)
defer cleanupStorage(storeDriver, testDir)
// put an unused blob
content := []byte("unused blob")
buf := bytes.NewBuffer(content)
unusedDigest := godigest.FromBytes(content)
_, _, err = imgStore.FullBlobUpload("repo", bytes.NewReader(buf.Bytes()), unusedDigest)
So(err, ShouldBeNil)
content = []byte("test-data1")
buf = bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
_, _, err = imgStore.FullBlobUpload("repo", bytes.NewReader(buf.Bytes()), digest)
So(err, ShouldBeNil)
cblob, cdigest := GetRandomImageConfig()
var clen int64
_, clen, err = imgStore.FullBlobUpload("repo", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
annotationsMap := make(map[string]string)
annotationsMap[ispec.AnnotationRefName] = tag
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
Annotations: annotationsMap,
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
manifestDigest, _, err := imgStore.PutImageManifest("repo", tag, ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
Convey("Try to delete blob currently in use", func() {
// layer blob
err := imgStore.DeleteBlob("repo", digest)
So(err, ShouldEqual, zerr.ErrBlobReferenced)
// manifest
err = imgStore.DeleteBlob("repo", manifestDigest)
So(err, ShouldEqual, zerr.ErrBlobReferenced)
// config
err = imgStore.DeleteBlob("repo", cdigest)
So(err, ShouldEqual, zerr.ErrBlobReferenced)
})
Convey("Delete unused blob", func() {
err := imgStore.DeleteBlob("repo", unusedDigest)
So(err, ShouldBeNil)
})
Convey("Delete manifest first, then blob", func() {
err := imgStore.DeleteImageManifest("repo", manifestDigest.String(), false)
So(err, ShouldBeNil)
err = imgStore.DeleteBlob("repo", digest)
So(err, ShouldBeNil)
// config
err = imgStore.DeleteBlob("repo", cdigest)
So(err, ShouldBeNil)
})
})
}
func TestGCSStorageAPIs(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(storeDriver, testDir)
Convey("Repo layout", t, func(c C) {
repoName := "test"
Convey("Get all blobs from repo without initialization", func() {
allBlobs, err := imgStore.GetAllBlobs(repoName)
So(err, ShouldBeNil)
So(allBlobs, ShouldBeEmpty)
ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(ok, ShouldBeFalse)
})
Convey("Validate repo without initialization", func() {
v, err := imgStore.ValidateRepo(repoName)
So(v, ShouldEqual, false)
So(err, ShouldNotBeNil)
ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(ok, ShouldBeFalse)
})
Convey("Initialize repo", func() {
err := imgStore.InitRepo(repoName)
So(err, ShouldBeNil)
ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(ok, ShouldBeTrue)
storeController := storage.StoreController{}
storeController.DefaultStore = imgStore
So(storeController.GetImageStore("test"), ShouldResemble, imgStore)
})
Convey("Validate repo", func() {
repos, err := imgStore.ValidateRepo(repoName)
So(err, ShouldBeNil)
So(repos, ShouldEqual, true)
})
Convey("Get repos", func() {
repos, err := imgStore.GetRepositories()
So(err, ShouldBeNil)
So(repos, ShouldNotBeEmpty)
repos, more, err := imgStore.GetNextRepositories("", -1, func(repo string) (bool, error) {
return true, nil
})
So(more, ShouldBeFalse)
So(err, ShouldBeNil)
So(repos, ShouldNotBeEmpty)
})
Convey("Get image tags", func() {
v, err := imgStore.GetImageTags("test")
So(err, ShouldBeNil)
So(v, ShouldBeEmpty)
})
Convey("Full blob upload unavailable algorithm", func() {
body := []byte("this blob will be hashed using an unavailable hashing algorithm")
buf := bytes.NewBuffer(body)
digest := godigest.Digest("md5:8114c3f59ef9dcf737410e0f4b00a154")
upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
So(err, ShouldEqual, godigest.ErrDigestUnsupported)
So(n, ShouldEqual, -1)
So(upload, ShouldEqual, "")
// Check no blobs are returned and there are no errors
// if other paths for different algorithms are missing
digests, err := imgStore.GetAllBlobs("test")
So(err, ShouldBeNil)
So(digests, ShouldBeEmpty)
})
Convey("Full blob upload", func() {
body := []byte("this is a blob")
buf := bytes.NewBuffer(body)
digest := godigest.FromBytes(body)
upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
So(err, ShouldBeNil)
So(n, ShouldEqual, len(body))
So(upload, ShouldNotBeEmpty)
err = imgStore.VerifyBlobDigestValue("test", digest)
So(err, ShouldBeNil)
// Check the blob is returned and there are no errors
// if other paths for different algorithms are missing
digests, err := imgStore.GetAllBlobs("test")
So(err, ShouldBeNil)
So(digests, ShouldContain, digest)
So(len(digests), ShouldEqual, 1)
})
Convey("Full blob upload sha512", func() {
body := []byte("this blob will be hashed using sha512")
buf := bytes.NewBuffer(body)
digest := godigest.SHA512.FromBytes(body)
upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
So(err, ShouldBeNil)
So(n, ShouldEqual, len(body))
So(upload, ShouldNotBeEmpty)
// Check the blob is returned and there are no errors
// if other paths for different algorithms are missing
digests, err := imgStore.GetAllBlobs("test")
So(err, ShouldBeNil)
So(digests, ShouldContain, digest)
// imgStore is reused so look for this digest and
// the ones uploaded by previous tests
So(len(digests), ShouldEqual, 2)
})
Convey("Full blob upload sha384", func() {
body := []byte("this blob will be hashed using sha384")
buf := bytes.NewBuffer(body)
digest := godigest.SHA384.FromBytes(body)
upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
So(err, ShouldBeNil)
So(n, ShouldEqual, len(body))
So(upload, ShouldNotBeEmpty)
// Check the blob is returned and there are no errors
// if other paths for different algorithms are missing
digests, err := imgStore.GetAllBlobs("test")
So(err, ShouldBeNil)
So(digests, ShouldContain, digest)
// imgStore is reused so look for this digest and
// the ones uploaded by previous tests
So(len(digests), ShouldEqual, 3)
})
Convey("New blob upload", func() {
upload, err := imgStore.NewBlobUpload("test")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
err = imgStore.DeleteBlobUpload("test", upload)
So(err, ShouldBeNil)
upload, err = imgStore.NewBlobUpload("test")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
Convey("Get blob upload", func() {
bupload, err := imgStore.GetBlobUpload("test", "invalid")
So(err, ShouldNotBeNil)
So(bupload, ShouldEqual, -1)
bupload, err = imgStore.GetBlobUpload("hi", " \255")
So(err, ShouldNotBeNil)
So(bupload, ShouldEqual, -1)
bupload, err = imgStore.GetBlobUpload("test", upload)
So(err, ShouldBeNil)
So(bupload, ShouldBeGreaterThanOrEqualTo, 0)
bupload, err = imgStore.BlobUploadInfo("test", upload)
So(err, ShouldBeNil)
So(bupload, ShouldBeGreaterThanOrEqualTo, 0)
content := []byte("test-data1")
firstChunkContent := []byte("test")
firstChunkBuf := bytes.NewBuffer(firstChunkContent)
secondChunkContent := []byte("-data1")
secondChunkBuf := bytes.NewBuffer(secondChunkContent)
firstChunkLen := firstChunkBuf.Len()
secondChunkLen := secondChunkBuf.Len()
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blobDigest := digest
// invalid chunk range
_, err = imgStore.PutBlobChunk("test", upload, 10, int64(buflen), buf)
So(err, ShouldNotBeNil)
bupload, err = imgStore.PutBlobChunk("test", upload, 0, int64(firstChunkLen), firstChunkBuf)
So(err, ShouldBeNil)
So(bupload, ShouldEqual, firstChunkLen)
bupload, err = imgStore.GetBlobUpload("test", upload)
So(err, ShouldBeNil)
So(bupload, ShouldEqual, int64(firstChunkLen))
bupload, err = imgStore.BlobUploadInfo("test", upload)
So(err, ShouldBeNil)
So(bupload, ShouldEqual, int64(firstChunkLen))
bupload, err = imgStore.PutBlobChunk("test", upload, int64(firstChunkLen), int64(buflen), secondChunkBuf)
So(err, ShouldBeNil)
So(bupload, ShouldEqual, int64(firstChunkLen+secondChunkLen))
err = imgStore.FinishBlobUpload("test", upload, buf, digest)
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("test", digest)
So(err, ShouldBeNil)
ok, _, _, err := imgStore.StatBlob("test", digest)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
blob, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
blobBuf := new(strings.Builder)
n, err := io.Copy(blobBuf, blob)
// check errors
So(n, ShouldEqual, buflen)
So(err, ShouldBeNil)
So(blobBuf.String(), ShouldEqual, buf.String())
blobContent, err := imgStore.GetBlobContent("test", digest)
So(err, ShouldBeNil)
So(blobContent, ShouldResemble, content)
err = blob.Close()
So(err, ShouldBeNil)
manifest := ispec.Manifest{}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
Convey("Bad image manifest", func() {
_, _, err = imgStore.PutImageManifest("test", digest.String(), "application/json",
manifestBuf, nil)
So(err, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
[]byte{}, nil)
So(err, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
[]byte(`{"test":true}`), nil)
So(err, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
manifestBuf, nil)
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest("test", digest.String())
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest("inexistent", digest.String())
So(err, ShouldNotBeNil)
})
Convey("Good image manifest", func() {
cblob, cdigest := GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob("test", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
annotationsMap := make(map[string]string)
annotationsMap[ispec.AnnotationRefName] = "1.0"
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
Annotations: annotationsMap,
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest := godigest.FromBytes(manifestBuf)
// bad manifest
manifest.Layers[0].Digest = godigest.FromBytes([]byte("inexistent"))
badMb, err := json.Marshal(manifest)
So(err, ShouldBeNil)
_, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, badMb, nil)
So(err, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
// same manifest for coverage
_, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, _, err = imgStore.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, err = imgStore.GetImageTags("inexistent")
So(err, ShouldNotBeNil)
// total tags should be 3 but they have same reference.
tags, err := imgStore.GetImageTags("test")
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 3)
_, _, _, err = imgStore.GetImageManifest("test", digest.String())
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("test", "3.0")
So(err, ShouldBeNil)
err = imgStore.DeleteImageManifest("test", "1.0", false)
So(err, ShouldBeNil)
tags, err = imgStore.GetImageTags("test")
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 2)
repos, err := imgStore.GetRepositories()
So(err, ShouldBeNil)
So(len(repos), ShouldEqual, 1)
So(repos[0], ShouldEqual, "test")
repos, more, err := imgStore.GetNextRepositories("", -1, func(repo string) (bool, error) {
return true, nil
})
So(err, ShouldBeNil)
So(more, ShouldBeFalse)
So(len(repos), ShouldEqual, 1)
So(repos[0], ShouldEqual, "test")
repos, more, err = imgStore.GetNextRepositories("", -1, func(repo string) (bool, error) {
return false, nil
})
So(err, ShouldBeNil)
So(more, ShouldBeFalse)
So(len(repos), ShouldEqual, 0)
// We deleted only one tag, make sure blob should not be removed.
hasBlob, _, err = imgStore.CheckBlob("test", digest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
// with detectManifestCollision should get error
err = imgStore.DeleteImageManifest("test", digest.String(), true)
So(err, ShouldNotBeNil)
// If we pass reference all manifest with input reference should be deleted.
err = imgStore.DeleteImageManifest("test", digest.String(), false)
So(err, ShouldBeNil)
tags, err = imgStore.GetImageTags("test")
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// All tags/references are deleted, blob should not be present in disk.
hasBlob, _, err = imgStore.CheckBlob("test", digest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
hasBlob, _, _, err = imgStore.StatBlob("test", digest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
err = imgStore.DeleteBlob("test", "inexistent")
So(err, ShouldNotBeNil)
err = imgStore.DeleteBlob("test", godigest.FromBytes([]byte("inexistent")))
So(err, ShouldNotBeNil)
err = imgStore.DeleteBlob("test", blobDigest)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("test", digest.String())
So(err, ShouldNotBeNil)
})
})
err = imgStore.DeleteBlobUpload("test", upload)
So(err, ShouldNotBeNil)
})
Convey("New blob upload streamed", func() {
bupload, err := imgStore.NewBlobUpload("test")
So(err, ShouldBeNil)
So(bupload, ShouldNotBeEmpty)
Convey("Get blob upload", func() {
upload, err := imgStore.GetBlobUpload("test", "invalid")
So(err, ShouldNotBeNil)
So(upload, ShouldEqual, -1)
upload, err = imgStore.GetBlobUpload("test", bupload)
So(err, ShouldBeNil)
So(upload, ShouldBeGreaterThanOrEqualTo, 0)
_, err = imgStore.BlobUploadInfo("test", "inexistent")
So(err, ShouldNotBeNil)
upload, err = imgStore.BlobUploadInfo("test", bupload)
So(err, ShouldBeNil)
So(upload, ShouldBeGreaterThanOrEqualTo, 0)
content := []byte("test-data2")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
upload, err = imgStore.PutBlobChunkStreamed("test", bupload, buf)
So(err, ShouldBeNil)
So(upload, ShouldEqual, buflen)
_, err = imgStore.PutBlobChunkStreamed("test", "inexistent", buf)
So(err, ShouldNotBeNil)
err = imgStore.FinishBlobUpload("test", "inexistent", buf, digest)
So(err, ShouldNotBeNil)
// invalid digest
err = imgStore.FinishBlobUpload("test", "inexistent", buf, "sha256:invalid")
So(err, ShouldNotBeNil)
err = imgStore.FinishBlobUpload("test", bupload, buf, digest)
So(err, ShouldBeNil)
ok, _, err := imgStore.CheckBlob("test", digest)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
ok, _, _, err = imgStore.StatBlob("test", digest)
So(ok, ShouldBeTrue)
So(err, ShouldBeNil)
_, _, err = imgStore.GetBlob("test", "inexistent", "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldNotBeNil)
blob, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
err = blob.Close()
So(err, ShouldBeNil)
blobContent, err := imgStore.GetBlobContent("test", digest)
So(err, ShouldBeNil)
So(content, ShouldResemble, blobContent)
_, err = imgStore.GetBlobContent("inexistent", digest)
So(err, ShouldNotBeNil)
manifest := ispec.Manifest{}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
Convey("Bad digests", func() {
_, _, err := imgStore.FullBlobUpload("test", bytes.NewBuffer([]byte{}), "inexistent")
So(err, ShouldNotBeNil)
_, _, err = imgStore.CheckBlob("test", "inexistent")
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.StatBlob("test", "inexistent")
So(err, ShouldNotBeNil)
})
Convey("Bad image manifest", func() {
_, _, err = imgStore.PutImageManifest("test", digest.String(),
ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest("test", digest.String(),
ispec.MediaTypeImageManifest, []byte("bad json"), nil)
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest("test", digest.String())
So(err, ShouldNotBeNil)
})
Convey("Good image manifest", func() {
cblob, cdigest := GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob("test", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest := godigest.FromBytes(manifestBuf)
_, _, err = imgStore.PutImageManifest("test", digest.String(),
ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
// same manifest for coverage
_, _, err = imgStore.PutImageManifest("test", digest.String(),
ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("test", digest.String())
So(err, ShouldBeNil)
_, err = imgStore.GetIndexContent("inexistent")
So(err, ShouldNotBeNil)
indexContent, err := imgStore.GetIndexContent("test")
So(err, ShouldBeNil)
var index ispec.Index
err = json.Unmarshal(indexContent, &index)
So(err, ShouldBeNil)
So(len(index.Manifests), ShouldEqual, 1)
err = imgStore.DeleteImageManifest("test", "1.0", false)
So(err, ShouldNotBeNil)
err = imgStore.DeleteImageManifest("inexistent", "1.0", false)
So(err, ShouldNotBeNil)
err = imgStore.DeleteImageManifest("test", digest.String(), false)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("test", digest.String())
So(err, ShouldNotBeNil)
})
})
err = imgStore.DeleteBlobUpload("test", bupload)
So(err, ShouldNotBeNil)
})
Convey("Modify manifest in-place", func() {
// original blob
upload, err := imgStore.NewBlobUpload("replace")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data-replace-1")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed("replace", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest1 := strings.Split(digest.String(), ":")[1]
So(blobDigest1, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("replace", upload, buf, digest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
cblob, cdigest := GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload("replace", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob("replace", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(manifestBuf)
_, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("replace", digest.String())
So(err, ShouldBeNil)
// new blob to replace
upload, err = imgStore.NewBlobUpload("replace")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content = []byte("test-data-replace-2")
buf = bytes.NewBuffer(content)
buflen = buf.Len()
digest = godigest.FromBytes(content)
blob, err = imgStore.PutBlobChunkStreamed("replace", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest2 := strings.Split(digest.String(), ":")[1]
So(blobDigest2, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("replace", upload, buf, digest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
cblob, cdigest = GetRandomImageConfig()
_, clen, err = imgStore.FullBlobUpload("replace", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err = imgStore.CheckBlob("replace", cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest = ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
_ = godigest.FromBytes(manifestBuf)
_, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
})
Convey("Locks", func() {
// in parallel, a mix of read and write locks - mainly for coverage
var wg sync.WaitGroup
for range 1000 {
wg.Add(2)
go func() {
var lockLatency time.Time
defer wg.Done()
imgStore.Lock(&lockLatency)
func() {}()
imgStore.Unlock(&lockLatency)
}()
go func() {
var lockLatency time.Time
defer wg.Done()
imgStore.RLock(&lockLatency)
func() {}()
imgStore.RUnlock(&lockLatency)
}()
}
wg.Wait()
})
})
}
func TestGCSReuploadCorruptedBlob(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
rawDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(rawDriver, testDir)
// Wrap driver for WriteFile access
gcsDriver := gcs.New(rawDriver)
Convey("Test errors paths", t, func() {
storeController := storage.StoreController{DefaultStore: imgStore}
image := CreateRandomImage()
err := WriteImageToFileSystem(image, repoName, tag, storeController)
So(err, ShouldBeNil)
})
Convey("Test reupload repair corrupted image", t, func() {
storeController := storage.StoreController{DefaultStore: imgStore}
image := CreateRandomImage()
err := WriteImageToFileSystem(image, repoName, tag, storeController)
So(err, ShouldBeNil)
blob := image.Layers[0]
blobDigest := godigest.FromBytes(blob)
blobSize := len(blob)
blobPath := imgStore.BlobPath(repoName, blobDigest)
ok, size, err := imgStore.CheckBlob(repoName, blobDigest)
So(ok, ShouldBeTrue)
So(size, ShouldEqual, blobSize)
So(err, ShouldBeNil)
_, err = gcsDriver.WriteFile(blobPath, []byte("corrupted"))
So(err, ShouldBeNil)
ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
So(ok, ShouldBeFalse)
So(size, ShouldNotEqual, blobSize)
So(err, ShouldEqual, zerr.ErrBlobNotFound)
err = WriteImageToFileSystem(image, repoName, tag, storeController)
So(err, ShouldBeNil)
ok, size, _, err = imgStore.StatBlob(repoName, blobDigest)
So(ok, ShouldBeTrue)
So(blobSize, ShouldEqual, size)
So(err, ShouldBeNil)
ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
So(ok, ShouldBeTrue)
So(size, ShouldEqual, blobSize)
So(err, ShouldBeNil)
})
Convey("Test reupload repair corrupted image index", t, func() {
storeController := storage.StoreController{DefaultStore: imgStore}
image := CreateRandomMultiarch()
tag := "index"
err := WriteMultiArchImageToFileSystem(image, repoName, tag, storeController)
So(err, ShouldBeNil)
blob := image.Images[0].Layers[0]
blobDigest := godigest.FromBytes(blob)
blobSize := len(blob)
blobPath := imgStore.BlobPath(repoName, blobDigest)
ok, size, err := imgStore.CheckBlob(repoName, blobDigest)
So(ok, ShouldBeTrue)
So(size, ShouldEqual, blobSize)
So(err, ShouldBeNil)
_, err = gcsDriver.WriteFile(blobPath, []byte("corrupted"))
So(err, ShouldBeNil)
ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
So(ok, ShouldBeFalse)
So(size, ShouldNotEqual, blobSize)
So(err, ShouldEqual, zerr.ErrBlobNotFound)
err = WriteMultiArchImageToFileSystem(image, repoName, tag, storeController)
So(err, ShouldBeNil)
ok, size, _, err = imgStore.StatBlob(repoName, blobDigest)
So(ok, ShouldBeTrue)
So(blobSize, ShouldEqual, size)
So(err, ShouldBeNil)
ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
So(ok, ShouldBeTrue)
So(size, ShouldEqual, blobSize)
So(err, ShouldBeNil)
})
}
func TestGCSStorageHandler(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
Convey("Test storage handler", t, func() {
firstRootDir := "/util_test1"
firstCacheDir := t.TempDir()
firstStorageDriver, firstStore, err := createObjectsStore(firstRootDir, firstCacheDir, true)
So(err, ShouldBeNil)
defer cleanupStorage(firstStorageDriver, firstRootDir)
secondRootDir := "/util_test2"
secondCacheDir := t.TempDir()
secondStorageDriver, secondStore, err := createObjectsStore(secondRootDir, secondCacheDir, true)
So(err, ShouldBeNil)
defer cleanupStorage(secondStorageDriver, secondRootDir)
thirdRootDir := "/util_test3"
thirdCacheDir := t.TempDir()
thirdStorageDriver, thirdStore, err := createObjectsStore(thirdRootDir, thirdCacheDir, true)
So(err, ShouldBeNil)
defer cleanupStorage(thirdStorageDriver, thirdRootDir)
storeController := storage.StoreController{}
storeController.DefaultStore = firstStore
subStore := make(map[string]storageTypes.ImageStore)
subStore["/a"] = secondStore
subStore["/b"] = thirdStore
storeController.SubStore = subStore
imgStore := storeController.GetImageStore("zot-x-test")
So(imgStore.RootDir(), ShouldEqual, firstRootDir)
imgStore = storeController.GetImageStore("a/zot-a-test")
So(imgStore.RootDir(), ShouldEqual, secondRootDir)
imgStore = storeController.GetImageStore("b/zot-b-test")
So(imgStore.RootDir(), ShouldEqual, thirdRootDir)
imgStore = storeController.GetImageStore("c/zot-c-test")
So(imgStore.RootDir(), ShouldEqual, firstRootDir)
})
}
func TestGCSMandatoryAnnotations(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
testLog := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, testLog)
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(storeDriver, testDir)
Convey("Setup manifest", t, func() {
content := []byte("test-data1")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
_, _, err = imgStore.FullBlobUpload("test", bytes.NewReader(buf.Bytes()), digest)
So(err, ShouldBeNil)
cblob, cdigest := GetRandomImageConfig()
var clen int64
_, clen, err = imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
annotationsMap := make(map[string]string)
annotationsMap[ispec.AnnotationRefName] = "1.0"
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
Annotations: annotationsMap,
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
Convey("Missing mandatory annotations", func() {
// Create imgStore with linter that returns false (missing annotations)
cacheDriver, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: tdir,
Name: "cache",
UseRelPaths: false,
}, testLog)
imgStoreWithLinter := gcs.NewImageStore(testDir, tdir, false, false, testLog, metrics,
&mocks.MockedLint{
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
return false, nil
},
}, storeDriver, cacheDriver, nil, nil)
_, _, err = imgStoreWithLinter.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldNotBeNil)
})
Convey("Error on mandatory annotations", func() {
// Create imgStore with linter that returns error
_, _ = storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: tdir,
Name: "cache",
UseRelPaths: false,
}, testLog)
imgStoreWithLinter := gcs.NewImageStore(testDir, tdir, false, false, testLog, metrics,
&mocks.MockedLint{
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
//nolint: err113
return false, errors.New("linter error")
},
}, storeDriver, nil, nil, nil)
_, _, err = imgStoreWithLinter.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldNotBeNil)
})
})
}
// pushRandomImageIndexGCS is a helper for GC tests.
func pushRandomImageIndexGCS(imgStore storageTypes.ImageStore, repoName string,
) (godigest.Digest, godigest.Digest, godigest.Digest, int64) {
content := []byte("this is a blob")
bdgst := godigest.FromBytes(content)
So(bdgst, ShouldNotBeNil)
_, bsize, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(content), bdgst)
So(err, ShouldBeNil)
So(bsize, ShouldEqual, len(content))
var index ispec.Index
index.SchemaVersion = 2
index.MediaType = ispec.MediaTypeImageIndex
var digest godigest.Digest
for range 4 {
// upload image config blob
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
cblob, cdigest := GetRandomImageConfig()
buf := bytes.NewBuffer(cblob)
buflen := buf.Len()
blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
// create a manifest
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayer,
Digest: bdgst,
Size: bsize,
},
},
}
manifest.SchemaVersion = 2
content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil)
So(err, ShouldBeNil)
index.Manifests = append(index.Manifests, ispec.Descriptor{
Digest: digest,
MediaType: ispec.MediaTypeImageManifest,
Size: int64(len(content)),
})
}
// upload index image
indexContent, err := json.Marshal(index)
So(err, ShouldBeNil)
indexDigest := godigest.FromBytes(indexContent)
So(indexDigest, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil)
So(err, ShouldBeNil)
return bdgst, digest, indexDigest, int64(len(indexContent))
}
func TestGCSGarbageCollectImageManifest(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
testLog := log.NewTestLogger()
audit := log.NewAuditLogger("debug", "")
ctx := context.Background()
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(storeDriver, testDir)
Convey("Garbage collect with short delay", t, func(c C) {
gcDelay := 1 * time.Second
garbageCollect := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
Delay: gcDelay,
ImageRetention: config.ImageRetention{
Delay: gcDelay,
Policies: []config.RetentionPolicy{
{
Repositories: []string{"**"},
DeleteReferrers: true,
DeleteUntagged: &trueVal,
},
},
},
}, audit, testLog)
// upload orphan blob
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data1")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
odigest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, odigest)
So(err, ShouldBeNil)
// sleep so orphan blob can be GC'ed
time.Sleep(1 * time.Second)
// upload blob
upload, err = imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content = []byte("test-data2")
buf = bytes.NewBuffer(content)
buflen = buf.Len()
bdigest := godigest.FromBytes(content)
blob, err = imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, bdigest)
So(err, ShouldBeNil)
annotationsMap := make(map[string]string)
annotationsMap[ispec.AnnotationRefName] = tag
cblob, cdigest := GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob(repoName, cdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: bdigest,
Size: int64(buflen),
},
},
Annotations: annotationsMap,
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
digest := godigest.FromBytes(manifestBuf)
_, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
// put artifact referencing above image
artifactBlob := []byte("artifact")
artifactBlobDigest := godigest.FromBytes(artifactBlob)
// push layer
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
So(err, ShouldBeNil)
// push config
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
ispec.DescriptorEmptyJSON.Digest)
So(err, ShouldBeNil)
artifactManifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Layers: []ispec.Descriptor{
{
MediaType: "application/octet-stream",
Digest: artifactBlobDigest,
Size: int64(len(artifactBlob)),
},
},
Config: ispec.DescriptorEmptyJSON,
Subject: &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: digest,
Size: int64(len(manifestBuf)),
},
}
artifactManifest.SchemaVersion = 2
artifactManifestBuf, err := json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactDigest := godigest.FromBytes(artifactManifestBuf)
// push artifact manifest
_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
// push orphan artifact (missing subject)
artifactManifest.Subject = &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromBytes([]byte("miss")),
Size: int64(30),
}
artifactManifest.ArtifactType = "application/orphan"
artifactManifestBuf, err = json.Marshal(artifactManifest)
So(err, ShouldBeNil)
orphanArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)
// push orphan artifact manifest
_, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
hasBlob, _, err = imgStore.CheckBlob(repoName, odigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
// sleep so orphan blob can be GC'ed
time.Sleep(1 * time.Second)
Convey("Garbage collect blobs after manifest is removed", func() {
err = imgStore.DeleteImageManifest(repoName, digest.String(), false)
So(err, ShouldBeNil)
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
// check artifacts are gc'ed
_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
So(err, ShouldNotBeNil)
// check it gc'ed repo
exists := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(exists, ShouldBeFalse)
})
})
}
func TestGCSGarbageCollectImageIndex(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
testLog := log.NewTestLogger()
audit := log.NewAuditLogger("debug", "")
ctx := context.Background()
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(storeDriver, testDir)
Convey("Garbage collect with short delay", t, func(c C) {
gcDelay := 2 * time.Second
imageRetentionDelay := 2 * time.Second
garbageCollect := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
Delay: gcDelay,
ImageRetention: config.ImageRetention{
Delay: imageRetentionDelay,
Policies: []config.RetentionPolicy{
{
Repositories: []string{"**"},
DeleteReferrers: true,
DeleteUntagged: &trueVal,
},
},
},
}, audit, testLog)
// upload orphan blob
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data1")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
odigest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, odigest)
So(err, ShouldBeNil)
bdgst, digest, indexDigest, indexSize := pushRandomImageIndexGCS(imgStore, repoName)
// put artifact referencing above image
artifactBlob := []byte("artifact")
artifactBlobDigest := godigest.FromBytes(artifactBlob)
// push layer
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
So(err, ShouldBeNil)
// push config
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
ispec.DescriptorEmptyJSON.Digest)
So(err, ShouldBeNil)
// push artifact manifest pointing to index
artifactManifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Layers: []ispec.Descriptor{
{
MediaType: "application/octet-stream",
Digest: artifactBlobDigest,
Size: int64(len(artifactBlob)),
},
},
Config: ispec.DescriptorEmptyJSON,
Subject: &ispec.Descriptor{
MediaType: ispec.MediaTypeImageIndex,
Digest: indexDigest,
Size: indexSize,
},
ArtifactType: "application/forIndex",
}
artifactManifest.SchemaVersion = 2
artifactManifestBuf, err := json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactDigest := godigest.FromBytes(artifactManifestBuf)
// push artifact manifest
_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
time.Sleep(2 * time.Second)
Convey("delete index manifest, references should not be persisted", func() {
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
err = imgStore.DeleteImageManifest(repoName, indexDigest.String(), false)
So(err, ShouldBeNil)
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldNotBeNil)
// orphan blob
hasBlob, _, err = imgStore.CheckBlob(repoName, odigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
hasBlob, _, err = imgStore.CheckBlob(repoName, bdgst)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
// check last manifest from index image
hasBlob, _, err = imgStore.CheckBlob(repoName, digest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
// check referrer is gc'ed
_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldNotBeNil)
hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
// check it gc'ed repo
exists := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(exists, ShouldBeFalse)
})
})
}
func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
testLog := log.NewTestLogger()
audit := log.NewAuditLogger("debug", "")
ctx := context.Background()
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
if err != nil {
panic(err)
}
defer cleanupStorage(storeDriver, testDir)
Convey("Garbage collect with short delay", t, func() {
gcDelay := 5 * time.Second
imageRetentionDelay := 5 * time.Second
garbageCollect := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
Delay: gcDelay,
ImageRetention: config.ImageRetention{
Delay: imageRetentionDelay,
Policies: []config.RetentionPolicy{
{
Repositories: []string{"**"},
DeleteReferrers: true,
DeleteUntagged: &trueVal,
},
},
},
}, audit, testLog)
// upload orphan blob
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data1")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
odigest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, odigest)
So(err, ShouldBeNil)
content = []byte("this is a blob")
bdgst := godigest.FromBytes(content)
So(bdgst, ShouldNotBeNil)
_, bsize, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(content), bdgst)
So(err, ShouldBeNil)
So(bsize, ShouldEqual, len(content))
artifactBlob := []byte("artifact")
artifactBlobDigest := godigest.FromBytes(artifactBlob)
// push layer
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
So(err, ShouldBeNil)
// push config
_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
ispec.DescriptorEmptyJSON.Digest)
So(err, ShouldBeNil)
var index ispec.Index
index.SchemaVersion = 2
index.MediaType = ispec.MediaTypeImageIndex
var digest godigest.Digest
for range 4 {
// upload image config blob
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
cblob, cdigest := GetRandomImageConfig()
buf := bytes.NewBuffer(cblob)
buflen := buf.Len()
blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
// create a manifest
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayer,
Digest: bdgst,
Size: bsize,
},
},
}
manifest.SchemaVersion = 2
content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil)
So(err, ShouldBeNil)
index.Manifests = append(index.Manifests, ispec.Descriptor{
Digest: digest,
MediaType: ispec.MediaTypeImageManifest,
Size: int64(len(content)),
})
// for each manifest inside index, push an artifact
artifactManifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Layers: []ispec.Descriptor{
{
MediaType: "application/octet-stream",
Digest: artifactBlobDigest,
Size: int64(len(artifactBlob)),
},
},
Config: ispec.DescriptorEmptyJSON,
Subject: &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: digest,
Size: int64(len(content)),
},
ArtifactType: "application/forManifestInInnerIndex",
}
artifactManifest.SchemaVersion = 2
artifactManifestBuf, err := json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactDigest := godigest.FromBytes(artifactManifestBuf)
// push artifact manifest
_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
}
// also add a new image index inside this one
var innerIndex ispec.Index
innerIndex.SchemaVersion = 2
innerIndex.MediaType = ispec.MediaTypeImageIndex
for range 3 {
// upload image config blob
upload, err := imgStore.NewBlobUpload(repoName)
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
cblob, cdigest := GetRandomImageConfig()
buf := bytes.NewBuffer(cblob)
buflen := buf.Len()
blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
// create a manifest
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayer,
Digest: bdgst,
Size: bsize,
},
},
}
manifest.SchemaVersion = 2
content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest := godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil)
So(err, ShouldBeNil)
innerIndex.Manifests = append(innerIndex.Manifests, ispec.Descriptor{
Digest: digest,
MediaType: ispec.MediaTypeImageManifest,
Size: int64(len(content)),
})
}
// upload inner index image
innerIndexContent, err := json.Marshal(index)
So(err, ShouldBeNil)
innerIndexDigest := godigest.FromBytes(innerIndexContent)
So(innerIndexDigest, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest(repoName, innerIndexDigest.String(),
ispec.MediaTypeImageIndex, innerIndexContent, nil)
So(err, ShouldBeNil)
// add inner index into root index
index.Manifests = append(index.Manifests, ispec.Descriptor{
Digest: innerIndexDigest,
MediaType: ispec.MediaTypeImageIndex,
Size: int64(len(innerIndexContent)),
})
// push root index
// upload index image
indexContent, err := json.Marshal(index)
So(err, ShouldBeNil)
indexDigest := godigest.FromBytes(indexContent)
So(indexDigest, ShouldNotBeNil)
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil)
So(err, ShouldBeNil)
artifactManifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Layers: []ispec.Descriptor{
{
MediaType: "application/octet-stream",
Digest: artifactBlobDigest,
Size: int64(len(artifactBlob)),
},
},
Config: ispec.DescriptorEmptyJSON,
Subject: &ispec.Descriptor{
MediaType: ispec.MediaTypeImageIndex,
Digest: indexDigest,
Size: int64(len(indexContent)),
},
ArtifactType: "application/forIndex",
}
artifactManifest.SchemaVersion = 2
artifactManifestBuf, err := json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactDigest := godigest.FromBytes(artifactManifestBuf)
// push artifact manifest
_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
artifactManifest.Subject = &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: digest,
Size: int64(len(content)),
}
artifactManifest.ArtifactType = "application/forManifestInIndex"
artifactManifestIndexBuf, err := json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactManifestIndexDigest := godigest.FromBytes(artifactManifestIndexBuf)
// push artifact manifest referencing a manifest from index image
_, _, err = imgStore.PutImageManifest(repoName, artifactManifestIndexDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestIndexBuf, nil)
So(err, ShouldBeNil)
artifactManifest.Subject = &ispec.Descriptor{
MediaType: ispec.MediaTypeImageIndex,
Digest: innerIndexDigest,
Size: int64(len(innerIndexContent)),
}
artifactManifest.ArtifactType = "application/forInnerIndex"
artifactManifestInnerIndexBuf, err := json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactManifestInnerIndexDigest := godigest.FromBytes(artifactManifestInnerIndexBuf)
// push artifact manifest referencing a manifest from index image
_, _, err = imgStore.PutImageManifest(repoName, artifactManifestInnerIndexDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestInnerIndexBuf, nil)
So(err, ShouldBeNil)
// push artifact manifest pointing to artifact above
artifactManifest.Subject = &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: artifactDigest,
Size: int64(len(artifactManifestBuf)),
}
artifactManifest.ArtifactType = "application/forArtifact"
artifactManifestBuf, err = json.Marshal(artifactManifest)
So(err, ShouldBeNil)
artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)
_, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
// push orphan artifact (missing subject)
artifactManifest.Subject = &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromBytes([]byte("miss")),
Size: int64(30),
}
artifactManifest.ArtifactType = "application/orphan"
artifactManifestBuf, err = json.Marshal(artifactManifest)
So(err, ShouldBeNil)
orphanArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)
// push orphan artifact manifest
_, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(),
ispec.MediaTypeImageManifest, artifactManifestBuf, nil)
So(err, ShouldBeNil)
hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
hasBlob, _, _, err = imgStore.StatBlob(repoName, bdgst)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
time.Sleep(5 * time.Second)
Convey("delete inner referenced manifest", func() {
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
// check orphan artifact is gc'ed
_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldBeNil)
err = imgStore.DeleteImageManifest(repoName, artifactDigest.String(), false)
So(err, ShouldBeNil)
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
So(err, ShouldBeNil)
})
Convey("delete index manifest, references should not be persisted", func() {
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
// check orphan artifact is gc'ed
_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldBeNil)
err = imgStore.DeleteImageManifest(repoName, indexDigest.String(), false)
So(err, ShouldBeNil)
err = garbageCollect.CleanRepo(ctx, repoName)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
So(err, ShouldNotBeNil)
// orphan blob
hasBlob, _, err = imgStore.CheckBlob(repoName, odigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
hasBlob, _, _, err = imgStore.StatBlob(repoName, odigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
// check artifact is gc'ed
_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
So(err, ShouldNotBeNil)
// check inner index artifact is gc'ed
_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestInnerIndexDigest.String())
So(err, ShouldNotBeNil)
// check last manifest from index image
hasBlob, _, err = imgStore.CheckBlob(repoName, digest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
So(err, ShouldNotBeNil)
hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
hasBlob, _, err = imgStore.CheckBlob(repoName, bdgst)
So(err, ShouldNotBeNil)
So(hasBlob, ShouldEqual, false)
// check it gc'ed repo
exists := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(exists, ShouldBeFalse)
})
})
}
func TestGCSCheckAllBlobsIntegrity(t *testing.T) {
tskip.SkipGCS(t)
ensureDummyGCSCreds(t)
Convey("test with GCS storage", t, func() {
uuid, err := guuid.NewV4()
So(err, ShouldBeNil)
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
So(err, ShouldBeNil)
defer cleanupStorage(storeDriver, testDir)
testLog := log.NewTestLogger()
RunGCSCheckAllBlobsIntegrityTests(t, imgStore, gcs.New(storeDriver), testLog)
})
}
func RunGCSCheckAllBlobsIntegrityTests( //nolint: thelper
t *testing.T, imgStore storageTypes.ImageStore, driver storageTypes.Driver, testLog log.Logger,
) {
Convey("Scrub only one repo", func() {
// initialize repo
err := imgStore.InitRepo(repoName)
So(err, ShouldBeNil)
ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
So(ok, ShouldBeTrue)
storeCtlr := storage.StoreController{}
storeCtlr.DefaultStore = imgStore
So(storeCtlr.GetImageStore(repoName), ShouldResemble, imgStore)
image := CreateRandomImage()
err = WriteImageToFileSystem(image, repoName, "1.0", storeCtlr)
So(err, ShouldBeNil)
Convey("Blobs integrity not affected", func() {
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 1.0 ok")
err = WriteMultiArchImageToFileSystem(CreateMultiarchWith().RandomImages(0).Build(), repoName, "2.0", storeCtlr)
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 1.0 ok")
So(actual, ShouldContainSubstring, "test 2.0 ok")
})
Convey("Blobs integrity with context done", func() {
buff := bytes.NewBufferString("")
ctx, cancel := context.WithCancel(context.Background())
cancel()
res, err := storeCtlr.CheckAllBlobsIntegrity(ctx)
res.PrintScrubResults(buff)
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldNotContainSubstring, "test 1.0 ok")
})
Convey("Manifest integrity affected", func() {
// get content of manifest file
content, _, _, err := imgStore.GetImageManifest(repoName, image.ManifestDescriptor.Digest.String())
So(err, ShouldBeNil)
// delete content of manifest file
manifestDig := image.ManifestDescriptor.Digest.Encoded()
manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
err = driver.Delete(manifestFile)
So(err, ShouldBeNil)
defer func() {
// put manifest content back to file
_, err = driver.WriteFile(manifestFile, content)
So(err, ShouldBeNil)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldNotContainSubstring, "affected")
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
So(len(index.Manifests), ShouldEqual, 1)
_, err = driver.WriteFile(manifestFile, []byte("invalid content"))
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
// verify error message
So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s invalid manifest content", manifestDig))
index, err = common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
So(len(index.Manifests), ShouldEqual, 1)
manifestDescriptor := index.Manifests[0]
_, _, err = storage.CheckManifestAndConfig(repoName, manifestDescriptor, []byte("invalid content"), imgStore)
So(err, ShouldNotBeNil)
})
Convey("Config integrity affected", func() {
// get content of config file
content, err := imgStore.GetBlobContent(repoName, image.ConfigDescriptor.Digest)
So(err, ShouldBeNil)
// delete content of config file
configDig := image.ConfigDescriptor.Digest.Encoded()
configFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", configDig)
err = driver.Delete(configFile)
So(err, ShouldBeNil)
defer func() {
// put config content back to file
_, err = driver.WriteFile(configFile, content)
So(err, ShouldBeNil)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s blob not found", configDig))
_, err = driver.WriteFile(configFile, []byte("invalid content"))
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s invalid server config", configDig))
})
Convey("Layers integrity affected", func() {
// get content of layer
content, err := imgStore.GetBlobContent(repoName, image.Manifest.Layers[0].Digest)
So(err, ShouldBeNil)
// delete content of layer file
layerDig := image.Manifest.Layers[0].Digest.Encoded()
layerFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", layerDig)
_, err = driver.WriteFile(layerFile, []byte(" "))
So(err, ShouldBeNil)
defer func() {
// put layer content back to file
_, err = driver.WriteFile(layerFile, content)
So(err, ShouldBeNil)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s bad blob digest", layerDig))
})
Convey("Layer not found", func() {
// get content of layer
digest := image.Manifest.Layers[0].Digest
content, err := imgStore.GetBlobContent(repoName, digest)
So(err, ShouldBeNil)
// change layer file permissions
layerDig := image.Manifest.Layers[0].Digest.Encoded()
repoDir := path.Join(imgStore.RootDir(), repoName)
layerFile := path.Join(repoDir, "/blobs/sha256", layerDig)
err = driver.Delete(layerFile)
So(err, ShouldBeNil)
defer func() {
_, err := driver.WriteFile(layerFile, content)
So(err, ShouldBeNil)
}()
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
So(len(index.Manifests), ShouldEqual, 1)
// get content of layer
imageRes := storage.CheckLayers(repoName, "1.0", []ispec.Descriptor{{Digest: digest}}, imgStore)
So(imageRes.Status, ShouldEqual, "affected")
So(imageRes.Error, ShouldEqual, "blob not found")
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s blob not found", layerDig))
})
Convey("Scrub index with missing manifest blob - graceful handling", func() {
// Create a multiarch image with multiple manifests
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
err = WriteMultiArchImageToFileSystem(multiarchImage, repoName, "2.0", storeCtlr)
So(err, ShouldBeNil)
// Get the index to find the index manifest digest
idx, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
// Find the index manifest
var indexManifestDesc ispec.Descriptor
for _, desc := range idx.Manifests {
if desc.MediaType == ispec.MediaTypeImageIndex {
indexManifestDesc = desc
break
}
}
// Get the index content to find the manifest digests within it
indexBlob, err := imgStore.GetBlobContent(repoName, indexManifestDesc.Digest)
So(err, ShouldBeNil)
var indexContent ispec.Index
err = json.Unmarshal(indexBlob, &indexContent)
So(err, ShouldBeNil)
// Delete one of the manifest blobs within the index (but not all)
missingManifestDig := indexContent.Manifests[0].Digest.Encoded()
missingManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", missingManifestDig)
err = driver.Delete(missingManifestFile)
So(err, ShouldBeNil)
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
// Should mark the index as affected due to missing manifest
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 2.0 affected")
// Should continue processing and report the missing manifest
So(actual, ShouldContainSubstring, missingManifestDig)
})
Convey("Scrub index with non-missing error on manifest blob via file permissions", func() {
// Skip for non-local storage
if driver.Name() != storageConstants.LocalStorageDriverName {
return
}
// Create a multiarch image with multiple manifests
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
err = WriteMultiArchImageToFileSystem(multiarchImage, repoName, "2.1", storeCtlr)
So(err, ShouldBeNil)
// Get the index to find the index manifest digest
idx, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
// Find the index manifest
var indexManifestDesc ispec.Descriptor
for _, desc := range idx.Manifests {
if desc.MediaType == ispec.MediaTypeImageIndex {
indexManifestDesc = desc
break
}
}
// Get the index content to find the manifest digests within it
indexBlob, err := imgStore.GetBlobContent(repoName, indexManifestDesc.Digest)
So(err, ShouldBeNil)
var indexContent ispec.Index
err = json.Unmarshal(indexBlob, &indexContent)
So(err, ShouldBeNil)
// Remove read permissions on one of the manifest blobs to cause a permission denied error (non-missing error)
manifestDig := indexContent.Manifests[0].Digest.Encoded()
manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
err = os.Chmod(manifestFile, 0o000)
So(err, ShouldBeNil)
// Restore permissions after test
defer func() {
_ = os.Chmod(manifestFile, 0o644)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
// Should mark the index as affected due to non-missing error on manifest
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 2.1 affected")
// Should report the manifest digest as affected blob
So(actual, ShouldContainSubstring, manifestDig)
// Should have "bad blob digest" error
So(actual, ShouldContainSubstring, "bad blob digest")
})
Convey("Scrub index", func() {
newImage := CreateRandomImage()
newManifestDigest := newImage.ManifestDescriptor.Digest
err = WriteImageToFileSystem(newImage, repoName, "2.0", storeCtlr)
So(err, ShouldBeNil)
idx, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(idx, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
var index ispec.Index
index.SchemaVersion = 2
index.Subject = &manifestDescriptor
index.Manifests = []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageManifest,
Digest: newManifestDigest,
Size: newImage.ManifestDescriptor.Size,
},
}
indexBlob, err := json.Marshal(index)
So(err, ShouldBeNil)
indexDigest, _, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob, nil)
So(err, ShouldBeNil)
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 1.0 ok")
So(actual, ShouldContainSubstring, "test ok")
// test scrub context done
buff = bytes.NewBufferString("")
ctx, cancel := context.WithCancel(context.Background())
cancel()
res, err = storeCtlr.CheckAllBlobsIntegrity(ctx)
res.PrintScrubResults(buff)
So(err, ShouldNotBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldNotContainSubstring, "test 1.0 ok")
So(actual, ShouldNotContainSubstring, "test ok")
// test scrub index - errors
manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", newManifestDigest.Encoded())
_, err = driver.WriteFile(manifestFile, []byte("invalid content"))
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test affected")
// delete content of manifest file
err = driver.Delete(manifestFile)
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test affected")
indexFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", indexDigest.Encoded())
err = driver.Delete(indexFile)
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 1.0 ok")
So(actual, ShouldNotContainSubstring, "test affected")
index.Manifests[0].MediaType = "invalid"
indexBlob, err = json.Marshal(index)
So(err, ShouldBeNil)
_, err = driver.WriteFile(indexFile, indexBlob)
So(err, ShouldBeNil)
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
_, _, err = storage.CheckManifestAndConfig(repoName, index.Manifests[0], []byte{}, imgStore)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrBadManifest)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test affected")
_, err = driver.WriteFile(indexFile, []byte("invalid cotent"))
So(err, ShouldBeNil)
defer func() {
err := driver.Delete(indexFile)
So(err, ShouldBeNil)
}()
buff = bytes.NewBufferString("")
res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test affected")
})
Convey("Manifest not found", func() {
// delete manifest file
manifestDig := image.ManifestDescriptor.Digest.Encoded()
manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
err = driver.Delete(manifestFile)
So(err, ShouldBeNil)
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldNotContainSubstring, fmt.Sprintf("test 1.0 affected %s blob not found", manifestDig))
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
So(len(index.Manifests), ShouldEqual, 1)
})
Convey("use the result of an already scrubed manifest which is the subject of the current manifest", func() {
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
err = WriteImageToFileSystem(CreateDefaultImageWith().Subject(&manifestDescriptor).Build(),
repoName, "0.0.1", storeCtlr)
So(err, ShouldBeNil)
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 1.0 ok")
So(actual, ShouldContainSubstring, "test 0.0.1 ok")
})
Convey("preserve affected status when CheckLayers would overwrite it", func() {
// Create an image with a subject
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
subjectImage := CreateDefaultImageWith().Subject(&manifestDescriptor).Build()
err = WriteImageToFileSystem(subjectImage, repoName, "0.0.3", storeCtlr)
So(err, ShouldBeNil)
// Delete the subject manifest to mark it as affected
subjectManifestDig := manifestDescriptor.Digest.Encoded()
subjectManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", subjectManifestDig)
err = driver.Delete(subjectManifestFile)
So(err, ShouldBeNil)
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
// The manifest with the missing subject should be marked as affected
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 0.0.3 affected")
// Even if CheckLayers would pass, the affected status from the missing subject should be preserved
So(actual, ShouldContainSubstring, subjectManifestDig)
})
Convey("the subject of the current manifest doesn't exist", func() {
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
err = WriteImageToFileSystem(CreateDefaultImageWith().Subject(&manifestDescriptor).Build(),
repoName, "0.0.2", storeCtlr)
So(err, ShouldBeNil)
// get content of manifest file
content, _, _, err := imgStore.GetImageManifest(repoName, manifestDescriptor.Digest.String())
So(err, ShouldBeNil)
// delete content of manifest file
manifestDig := image.ManifestDescriptor.Digest.Encoded()
manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
err = driver.Delete(manifestFile)
So(err, ShouldBeNil)
defer func() {
// put manifest content back to file
_, err = driver.WriteFile(manifestFile, content)
So(err, ShouldBeNil)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 0.0.2 affected")
})
Convey("the subject of the current index doesn't exist", func() {
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
err = WriteMultiArchImageToFileSystem(CreateMultiarchWith().RandomImages(1).Subject(&manifestDescriptor).Build(),
repoName, "0.0.2", storeCtlr)
So(err, ShouldBeNil)
// get content of manifest file
content, _, _, err := imgStore.GetImageManifest(repoName, manifestDescriptor.Digest.String())
So(err, ShouldBeNil)
// delete content of manifest file
manifestDig := image.ManifestDescriptor.Digest.Encoded()
manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
err = driver.Delete(manifestFile)
So(err, ShouldBeNil)
defer func() {
// put manifest content back to file
_, err = driver.WriteFile(manifestFile, content)
So(err, ShouldBeNil)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 0.0.2 affected")
})
Convey("test errors", func() {
mockedImgStore := mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{repoName}, nil
},
ValidateRepoFn: func(name string) (bool, error) {
return false, nil
},
}
storeController := storage.StoreController{}
storeController.DefaultStore = mockedImgStore
_, err := storeController.CheckAllBlobsIntegrity(context.Background())
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrRepoBadLayout)
mockedImgStore = mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{repoName}, nil
},
GetIndexContentFn: func(repo string) ([]byte, error) {
return []byte{}, errUnexpectedError
},
}
storeController.DefaultStore = mockedImgStore
_, err = storeController.CheckAllBlobsIntegrity(context.Background())
So(err, ShouldNotBeNil)
So(err, ShouldEqual, errUnexpectedError)
manifestDigest := godigest.FromString("abcd")
mockedImgStore = mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{repoName}, nil
},
GetIndexContentFn: func(repo string) ([]byte, error) {
var index ispec.Index
index.SchemaVersion = 2
index.Manifests = []ispec.Descriptor{
{
MediaType: "InvalidMediaType",
Digest: manifestDigest,
Size: int64(100),
Annotations: map[string]string{ispec.AnnotationRefName: "1.0"},
},
}
return json.Marshal(index)
},
}
storeController.DefaultStore = mockedImgStore
res, err := storeController.CheckAllBlobsIntegrity(context.Background())
So(err, ShouldBeNil)
buff := bytes.NewBufferString("")
res.PrintScrubResults(buff)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, fmt.Sprintf("%s 1.0 affected %s invalid manifest content",
repoName, manifestDigest.Encoded()))
})
Convey("scrub with non-missing error on manifest subject blob via file permissions", func() {
// Skip for non-local storage
if driver.Name() != storageConstants.LocalStorageDriverName {
return
}
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
// Create an image with a subject
subjectImage := CreateDefaultImageWith().Subject(&manifestDescriptor).Build()
err = WriteImageToFileSystem(subjectImage, repoName, "0.0.6", storeCtlr)
So(err, ShouldBeNil)
// Get the subject manifest digest
subjectManifestDig := manifestDescriptor.Digest.Encoded()
subjectManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", subjectManifestDig)
// Remove read permissions to cause a permission denied error (non-missing error)
err = os.Chmod(subjectManifestFile, 0o000)
So(err, ShouldBeNil)
// Restore permissions after test
defer func() {
_ = os.Chmod(subjectManifestFile, 0o644)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
// Should mark the manifest as affected due to non-missing error on subject
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 0.0.6 affected")
// Should report the subject digest as affected blob
So(actual, ShouldContainSubstring, subjectManifestDig)
// Should have "bad blob digest" error
So(actual, ShouldContainSubstring, "bad blob digest")
})
Convey("scrub with non-missing error on index subject blob via file permissions", func() {
// Skip for non-local storage
if driver.Name() != storageConstants.LocalStorageDriverName {
return
}
index, err := common.GetIndex(imgStore, repoName, testLog)
So(err, ShouldBeNil)
manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
So(ok, ShouldBeTrue)
// Create a multiarch image with a subject
err = WriteMultiArchImageToFileSystem(CreateMultiarchWith().RandomImages(1).Subject(&manifestDescriptor).Build(),
repoName, "0.0.7", storeCtlr)
So(err, ShouldBeNil)
// Get the subject manifest digest
subjectManifestDig := manifestDescriptor.Digest.Encoded()
subjectManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", subjectManifestDig)
// Remove read permissions to cause a permission denied error (non-missing error)
err = os.Chmod(subjectManifestFile, 0o000)
So(err, ShouldBeNil)
// Restore permissions after test
defer func() {
_ = os.Chmod(subjectManifestFile, 0o644)
}()
buff := bytes.NewBufferString("")
res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
res.PrintScrubResults(buff)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
// Should mark the index as affected due to non-missing error on subject
So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
So(actual, ShouldContainSubstring, "test 0.0.7 affected")
// Should report the subject digest as affected blob
So(actual, ShouldContainSubstring, subjectManifestDig)
// Should have "bad blob digest" error
So(actual, ShouldContainSubstring, "bad blob digest")
})
})
}