Files
Jacob McSwain 273b15364b metadb: add optional fast restart path that skips storage walk when (version + commit + storage config) matches metaDB stamp (#4026)
* chore(metadb): add writer version to interface

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* chore(metadb): add writer version to db mock

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* chore(metadb): implement writer version for bolt, redis, and dynamodb

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* feat(metadb): add optional fast restart path that skips storage walk when binary identity matches metaDB stamp

binary identity is determined by the current release tag/commit and stored in metaDB after a successful storage parse. When fast restart is enabled, the next startup will skip the parse if the stored identity matches the current binary

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* chore(cli): serve: add a way to force reparse storage

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* refactor(meta): version: split to avoid global state mutation in tests

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(meta): version: include commit in writerVersion to distinguish retags

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* chore(config): add IsFastRestartEnabled() test

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(meta): skip writer-version stamp when storage parse is incomplete

ParseStorage returns nil even when individual repos fail to parse or are only partially parsed (a missing manifest blob), so MaybeParseStorage would stamp a partially-populated metaDB as good. On the next restart fastRestart trusts the stamp, skips the storage walk, and never recovers.

Track per-repo outcomes via parseStats and stamp only when the walk fully populated the metaDB, otherwise log and continue so the next restart reparses

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(docs): readme: remove trailing comma from JSON config

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(meta): dynamodb: use context.Background instead of context.TODO

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(meta): invalidate fast restart on storage config changes

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* chore(meta): dynamodb: use context.Background() instead of context.TODO()

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* docs(meta): dynamodb: add comment about nil AttributeValue handling in GetWriterVersion

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* chore: rename writer-version stamp to fast-restart stamp

also replaces the version/commit tracking to use BinaryVersion instead of WriterVersion

This should make things more clear

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(config): ensure FastRestart is on GlobalStorageConfig

This is not a per-subpath setting

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

* fix(metadb): redis: tests: ensure clients are closed

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>

---------

Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
2026-06-09 10:47:20 -07:00

1283 lines
40 KiB
Go

package boltdb_test
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"math"
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/log"
"zotregistry.dev/zot/v2/pkg/meta/boltdb"
proto_go "zotregistry.dev/zot/v2/pkg/meta/proto/gen"
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext"
. "zotregistry.dev/zot/v2/pkg/test/image-utils"
)
type imgTrustStore struct{}
func (its imgTrustStore) VerifySignature(
signatureType string, rawSignature []byte, sigKey string, manifestDigest godigest.Digest, imageMeta mTypes.ImageMeta,
repo string,
) (mTypes.Author, mTypes.ExpiryDate, mTypes.Validity, error) {
return "", time.Time{}, false, nil
}
var errImageMetaBucketNotFound = errors.New("ImageMeta bucket not found")
func TestWrapperErrors(t *testing.T) {
image := CreateDefaultImage()
imageMeta := image.AsImageMeta()
multiarchImageMeta := CreateMultiarchWith().Images([]Image{image}).Build().AsImageMeta()
badProtoBlob := []byte("bad-repo-meta")
goodRepoMetaBlob, err := proto.Marshal(&proto_go.RepoMeta{Name: "repo"})
if err != nil {
t.FailNow()
}
Convey("Errors", t, func() {
tmpDir := t.TempDir()
boltDBParams := boltdb.DBParameters{RootDir: tmpDir}
boltDriver, err := boltdb.GetBoltDriver(boltDBParams)
So(err, ShouldBeNil)
log := log.NewTestLogger()
boltdbWrapper, err := boltdb.New(boltDriver, log)
So(boltdbWrapper, ShouldNotBeNil)
So(err, ShouldBeNil)
boltdbWrapper.SetImageTrustStore(imgTrustStore{})
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("test")
ctx := userAc.DeriveContext(context.Background())
Convey("RemoveRepoReference", func() {
Convey("getProtoRepoMeta errors", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest)
So(err, ShouldNotBeNil)
})
Convey("getProtoImageMeta errors", func() {
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"tag": {
MediaType: ispec.MediaTypeImageManifest,
Digest: imageMeta.Digest.String(),
},
},
})
So(err, ShouldBeNil)
err = setImageMeta(imageMeta.Digest, badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest)
So(err, ShouldNotBeNil)
})
Convey("unmarshalProtoRepoBlobs errors", func() {
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldBeNil)
err = setRepoBlobInfo("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest)
So(err, ShouldNotBeNil)
})
})
Convey("UpdateSignaturesValidity", func() {
boltdbWrapper.SetImageTrustStore(imgTrustStore{})
digest := image.Digest()
ctx := context.Background()
Convey("image meta blob not found", func() {
err := boltdbWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
So(err, ShouldBeNil)
})
Convey("image meta unmarshal fail", func() {
err := setImageMeta(digest, badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
So(err, ShouldNotBeNil)
})
Convey("repo meta blob not found", func() {
err := boltdbWrapper.SetImageMeta(digest, imageMeta)
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
So(err, ShouldNotBeNil)
})
Convey("repo meta unmarshal fail", func() {
err := boltdbWrapper.SetImageMeta(digest, imageMeta)
So(err, ShouldBeNil)
err = setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
So(err, ShouldNotBeNil)
})
})
Convey("GetRepoLastUpdated", func() {
Convey("bad blob in db", func() {
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoBlobsBuck := tx.Bucket([]byte(boltdb.RepoBlobsBuck))
lastUpdatedBuck := repoBlobsBuck.Bucket([]byte(boltdb.RepoLastUpdatedBuck))
return lastUpdatedBuck.Put([]byte("repo"), []byte("bad-blob"))
})
So(err, ShouldBeNil)
lastUpdated := boltdbWrapper.GetRepoLastUpdated("repo")
So(lastUpdated, ShouldEqual, time.Time{})
})
})
Convey("UpdateStatsOnDownload", func() {
Convey("repo meta not found", func() {
err = boltdbWrapper.UpdateStatsOnDownload("repo", "ref")
So(err, ShouldNotBeNil)
})
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateStatsOnDownload("repo", "ref")
So(err, ShouldNotBeNil)
})
Convey("ref is tag and tag is not found", func() {
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateStatsOnDownload("repo", "not-found-tag")
So(err, ShouldNotBeNil)
})
Convey("digest not found in statistics", func() {
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldBeNil)
err = boltdbWrapper.UpdateStatsOnDownload("repo", godigest.FromString("not-found").String())
So(err, ShouldNotBeNil)
})
Convey("statistics entry missing but digest exists in tags - should create and increment", func() {
// Set repo reference to create tag
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldBeNil)
// Manually remove Statistics entry to simulate missing Statistics
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoMetaBuck := tx.Bucket([]byte(boltdb.RepoMetaBuck))
repoMetaBlob := repoMetaBuck.Get([]byte("repo"))
if len(repoMetaBlob) == 0 {
return zerr.ErrRepoMetaNotFound
}
var protoRepoMeta proto_go.RepoMeta
err := proto.Unmarshal(repoMetaBlob, &protoRepoMeta)
if err != nil {
return err
}
// Remove Statistics entry for the digest
delete(protoRepoMeta.Statistics, imageMeta.Digest.String())
repoMetaBlob, err = proto.Marshal(&protoRepoMeta)
if err != nil {
return err
}
return repoMetaBuck.Put([]byte("repo"), repoMetaBlob)
})
So(err, ShouldBeNil)
// Verify Statistics entry doesn't exist
repoMeta, err := boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldBeNil)
_, exists := repoMeta.Statistics[imageMeta.Digest.String()]
So(exists, ShouldBeFalse)
// Update stats - should create Statistics entry and increment
err = boltdbWrapper.UpdateStatsOnDownload("repo", "tag")
So(err, ShouldBeNil)
// Verify Statistics entry was created and incremented
repoMeta, err = boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldBeNil)
stats, exists := repoMeta.Statistics[imageMeta.Digest.String()]
So(exists, ShouldBeTrue)
So(stats.DownloadCount, ShouldEqual, 1)
// Update stats again - should increment existing entry
err = boltdbWrapper.UpdateStatsOnDownload("repo", "tag")
So(err, ShouldBeNil)
repoMeta, err = boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldBeNil)
stats, exists = repoMeta.Statistics[imageMeta.Digest.String()]
So(exists, ShouldBeTrue)
So(stats.DownloadCount, ShouldEqual, 2)
})
Convey("statistics entry missing but digest exists in tags - using digest reference", func() {
// Set repo reference to create tag
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldBeNil)
// Manually remove Statistics entry
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoMetaBuck := tx.Bucket([]byte(boltdb.RepoMetaBuck))
repoMetaBlob := repoMetaBuck.Get([]byte("repo"))
if len(repoMetaBlob) == 0 {
return zerr.ErrRepoMetaNotFound
}
var protoRepoMeta proto_go.RepoMeta
err := proto.Unmarshal(repoMetaBlob, &protoRepoMeta)
if err != nil {
return err
}
delete(protoRepoMeta.Statistics, imageMeta.Digest.String())
repoMetaBlob, err = proto.Marshal(&protoRepoMeta)
if err != nil {
return err
}
return repoMetaBuck.Put([]byte("repo"), repoMetaBlob)
})
So(err, ShouldBeNil)
// Update stats using digest directly
err = boltdbWrapper.UpdateStatsOnDownload("repo", imageMeta.Digest.String())
So(err, ShouldBeNil)
// Verify Statistics entry was created
repoMeta, err := boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldBeNil)
stats, exists := repoMeta.Statistics[imageMeta.Digest.String()]
So(exists, ShouldBeTrue)
So(stats.DownloadCount, ShouldEqual, 1)
})
})
Convey("GetReferrersInfo", func() {
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetReferrersInfo("repo", "refDig", []string{})
So(err, ShouldNotBeNil)
})
})
Convey("ResetRepoReferences", func() {
Convey("repo doesn't exist - returns early without error", func() {
ctx := context.Background()
// Verify repo doesn't exist
_, err := boltdbWrapper.GetRepoMeta(ctx, "nonexistent-repo")
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue)
// ResetRepoReferences should return early without error
err = boltdbWrapper.ResetRepoReferences("nonexistent-repo", nil)
So(err, ShouldBeNil)
// Verify repo still doesn't exist
_, err = boltdbWrapper.GetRepoMeta(ctx, "nonexistent-repo")
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue)
})
Convey("repo doesn't exist with tagsToKeep - returns early without error", func() {
ctx := context.Background()
// Verify repo doesn't exist
_, err := boltdbWrapper.GetRepoMeta(ctx, "nonexistent-repo2")
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue)
// ResetRepoReferences should return early without error even with tagsToKeep
tagsToKeep := map[string]bool{"tag1": true}
err = boltdbWrapper.ResetRepoReferences("nonexistent-repo2", tagsToKeep)
So(err, ShouldBeNil)
// Verify repo still doesn't exist
_, err = boltdbWrapper.GetRepoMeta(ctx, "nonexistent-repo2")
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue)
})
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.ResetRepoReferences("repo", nil)
So(err, ShouldNotBeNil)
})
Convey("preserve tags in tagsToKeep", func() {
ctx := context.Background()
// Create repo with multiple tags
image1 := CreateRandomImage()
image2 := CreateRandomImage()
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta())
So(err, ShouldBeNil)
// Wait a bit to ensure different timestamps
time.Sleep(10 * time.Millisecond)
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta())
So(err, ShouldBeNil)
// Get repo meta to capture TaggedTimestamp
repoMeta, err := boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldBeNil)
So(repoMeta.Tags, ShouldContainKey, "tag1")
So(repoMeta.Tags, ShouldContainKey, "tag2")
tag1Timestamp := repoMeta.Tags["tag1"].TaggedTimestamp
// Reset with only tag1 in tagsToKeep
tagsToKeep := map[string]bool{"tag1": true}
err = boltdbWrapper.ResetRepoReferences("repo", tagsToKeep)
So(err, ShouldBeNil)
// Verify tag1 is preserved with its timestamp
repoMeta, err = boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldBeNil)
So(repoMeta.Tags, ShouldContainKey, "tag1")
So(repoMeta.Tags, ShouldNotContainKey, "tag2")
So(repoMeta.Tags["tag1"].TaggedTimestamp, ShouldEqual, tag1Timestamp)
})
})
Convey("DecrementRepoStars", func() {
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.DecrementRepoStars("repo")
So(err, ShouldNotBeNil)
})
})
Convey("IncrementRepoStars", func() {
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.IncrementRepoStars("repo")
So(err, ShouldNotBeNil)
})
})
Convey("DeleteSignature", func() {
Convey("repo meta not found", func() {
err = boltdbWrapper.DeleteSignature("repo", godigest.FromString("dig"), mTypes.SignatureMetadata{})
So(err, ShouldNotBeNil)
})
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.DeleteSignature("repo", godigest.FromString("dig"), mTypes.SignatureMetadata{})
So(err, ShouldNotBeNil)
})
})
Convey("AddManifestSignature", func() {
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.AddManifestSignature("repo", godigest.FromString("dig"), mTypes.SignatureMetadata{})
So(err, ShouldNotBeNil)
})
})
Convey("GetMultipleRepoMeta", func() {
Convey("unmarshalProtoRepoMeta error", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true })
So(err, ShouldNotBeNil)
})
})
Convey("GetFullImageMeta", func() {
Convey("repo meta not found", func() {
_, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
So(err, ShouldNotBeNil)
})
Convey("unmarshalProtoRepoMeta fails", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
So(err, ShouldNotBeNil)
})
Convey("tag not found", func() {
err := setRepoMeta("repo", goodRepoMetaBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag-not-found")
So(err, ShouldNotBeNil)
})
Convey("getProtoImageMeta fails", func() {
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"tag": {
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromString("not-found").String(),
},
},
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
So(err, ShouldNotBeNil)
})
Convey("image is index, missing manifests are skipped gracefully", func() {
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
So(err, ShouldBeNil)
// Missing manifests are skipped gracefully, so GetFullImageMeta succeeds
// but returns an index with no manifests
fullImageMeta, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
So(err, ShouldBeNil)
So(len(fullImageMeta.Manifests), ShouldEqual, 0)
})
Convey("image is index, corrupted manifest data returns error", func() {
// Create a multiarch image with multiple manifests
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
multiarchImageMeta := multiarchImage.AsImageMeta()
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
So(err, ShouldBeNil)
// Store the first manifest normally
firstManifest := multiarchImage.Images[0]
firstManifestMeta := firstManifest.AsImageMeta()
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
So(err, ShouldBeNil)
// Store the second manifest normally first, then corrupt it
secondManifest := multiarchImage.Images[1]
secondManifestMeta := secondManifest.AsImageMeta()
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
So(err, ShouldBeNil)
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
// Corrupt the data for the second manifest by storing invalid protobuf data
// This will cause getProtoImageMeta to return an unmarshaling error
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
corruptedData := []byte("invalid protobuf data")
// Access BoltDB directly to corrupt the data
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
if imageBuck == nil {
return errImageMetaBucketNotFound
}
// Store corrupted protobuf data
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
})
So(err, ShouldBeNil)
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
So(err, ShouldBeNil)
// GetFullImageMeta should return an error due to corrupted manifest data
// The error from getAllContainedMeta should propagate
fullImageMeta, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
So(err, ShouldNotBeNil)
// Should still return a FullImageMeta object (even with error)
So(fullImageMeta, ShouldNotBeNil)
})
})
Convey("FilterRepos", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.FilterRepos(ctx, mTypes.AcceptAllRepoNames, mTypes.AcceptAllRepoMeta)
So(err, ShouldNotBeNil)
})
Convey("SearchTags", func() {
Convey("unmarshalProtoRepoMeta fails", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
// manifests are missing
_, err = boltdbWrapper.SearchTags(ctx, "repo:")
So(err, ShouldNotBeNil)
})
Convey("found repo meta", func() {
Convey("bad image manifest", func() {
badImageDigest := godigest.FromString("bad-image-manifest")
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"bad-image-manifest": {
MediaType: ispec.MediaTypeImageManifest,
Digest: badImageDigest.String(),
},
},
})
So(err, ShouldBeNil)
err = setImageMeta(badImageDigest, badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.SearchTags(ctx, "repo:")
So(err, ShouldNotBeNil)
})
Convey("bad image index", func() {
badIndexDigest := godigest.FromString("bad-image-manifest")
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"bad-image-index": {
MediaType: ispec.MediaTypeImageIndex,
Digest: badIndexDigest.String(),
},
},
})
So(err, ShouldBeNil)
err = setImageMeta(badIndexDigest, badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.SearchTags(ctx, "repo:")
So(err, ShouldNotBeNil)
})
Convey("good image index, bad inside manifest", func() {
goodIndexBadManifestDigest := godigest.FromString("good-index-bad-manifests")
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"good-index-bad-manifests": {
MediaType: ispec.MediaTypeImageIndex,
Digest: goodIndexBadManifestDigest.String(),
},
},
})
So(err, ShouldBeNil)
err = boltdbWrapper.SetImageMeta(goodIndexBadManifestDigest, multiarchImageMeta)
So(err, ShouldBeNil)
err = setImageMeta(image.Digest(), badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.SearchTags(ctx, "repo:")
So(err, ShouldNotBeNil)
})
Convey("bad media type", func() {
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"mad-media-type": {
MediaType: "bad media type",
Digest: godigest.FromString("dig").String(),
},
},
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.SearchTags(ctx, "repo:")
So(err, ShouldBeNil)
})
})
})
Convey("FilterTags", func() {
Convey("unmarshalProtoRepoMeta fails", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
_, err = boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
So(err, ShouldNotBeNil)
})
Convey("bad media Type fails", func() {
err := boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
Name: "repo",
Tags: map[mTypes.Tag]mTypes.Descriptor{
"bad-repo-meta": {
MediaType: "bad media type",
Digest: godigest.FromString("dig").String(),
},
},
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
So(err, ShouldBeNil)
})
Convey("getAllContainedMeta error for index is joined and processing continues", func() {
// Create a multiarch image with multiple manifests
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
multiarchImageMeta := multiarchImage.AsImageMeta()
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
So(err, ShouldBeNil)
// Store the first manifest normally
firstManifest := multiarchImage.Images[0]
firstManifestMeta := firstManifest.AsImageMeta()
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
So(err, ShouldBeNil)
// Store the second manifest normally first, then corrupt it
secondManifest := multiarchImage.Images[1]
secondManifestMeta := secondManifest.AsImageMeta()
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
So(err, ShouldBeNil)
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
// Corrupt the data for the second manifest by storing invalid protobuf data
// This will cause getProtoImageMeta to return an unmarshaling error
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
corruptedData := []byte("invalid protobuf data")
// Access BoltDB directly to corrupt the data
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
if imageBuck == nil {
return errImageMetaBucketNotFound
}
// Store corrupted protobuf data
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
})
So(err, ShouldBeNil)
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
So(err, ShouldBeNil)
// FilterTags should return an error due to corrupted manifest data
// The error from getAllContainedMeta should be joined with viewError
images, err := boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
So(err, ShouldNotBeNil)
// Should still return some images (the first valid manifest might be processed)
So(images, ShouldNotBeNil)
})
})
Convey("SearchRepos", func() {
Convey("unmarshalProtoRepoMeta fails", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
// manifests are missing
_, err = boltdbWrapper.SearchRepos(ctx, "repo")
So(err, ShouldNotBeNil)
})
})
Convey("FilterImageMeta", func() {
Convey("MediaType ImageIndex, getProtoImageMeta fails", func() {
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
So(err, ShouldBeNil)
err = setImageMeta(image.Digest(), badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
// manifests are missing
_, err = boltdbWrapper.FilterImageMeta(ctx, []string{multiarchImageMeta.Digest.String()})
So(err, ShouldNotBeNil)
})
})
Convey("GetImageMeta", func() {
Convey("image is index, getAllContainedMeta error returns error", func() {
// Create a multiarch image with multiple manifests
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
multiarchImageMeta := multiarchImage.AsImageMeta()
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
So(err, ShouldBeNil)
// Store the first manifest normally
firstManifest := multiarchImage.Images[0]
firstManifestMeta := firstManifest.AsImageMeta()
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
So(err, ShouldBeNil)
// Store the second manifest normally first, then corrupt it
secondManifest := multiarchImage.Images[1]
secondManifestMeta := secondManifest.AsImageMeta()
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
So(err, ShouldBeNil)
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
// Corrupt the data for the second manifest by storing invalid protobuf data
// This will cause getProtoImageMeta to return an unmarshaling error
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
corruptedData := []byte("invalid protobuf data")
// Access BoltDB directly to corrupt the data
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
if imageBuck == nil {
return errImageMetaBucketNotFound
}
// Store corrupted protobuf data
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
})
So(err, ShouldBeNil)
// GetImageMeta should return an error due to corrupted manifest data
// The error from getAllContainedMeta should propagate
imageMeta, err := boltdbWrapper.GetImageMeta(multiarchImageMeta.Digest)
So(err, ShouldNotBeNil)
// Should still return an ImageMeta object (even with error)
So(imageMeta, ShouldNotBeNil)
})
})
Convey("SetRepoReference", func() {
Convey("getProtoRepoMeta errors", func() {
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldNotBeNil)
})
Convey("unmarshalProtoRepoBlobs errors", func() {
err := setRepoMeta("repo", goodRepoMetaBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = setRepoBlobInfo("repo", badProtoBlob, boltdbWrapper.DB)
So(err, ShouldBeNil)
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
So(err, ShouldNotBeNil)
})
})
Convey("AddUserAPIKey", func() {
Convey("no userid found", func() {
userAc := reqCtx.NewUserAccessControl()
ctx := userAc.DeriveContext(context.Background())
err = boltdbWrapper.AddUserAPIKey(ctx, "", &mTypes.APIKeyDetails{})
So(err, ShouldNotBeNil)
})
err = boltdbWrapper.AddUserAPIKey(ctx, "", &mTypes.APIKeyDetails{})
So(err, ShouldNotBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserDataBucket))
})
So(err, ShouldBeNil)
err = boltdbWrapper.AddUserAPIKey(ctx, "test", &mTypes.APIKeyDetails{})
So(err, ShouldNotBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserAPIKeysBucket))
})
So(err, ShouldBeNil)
err = boltdbWrapper.AddUserAPIKey(ctx, "", &mTypes.APIKeyDetails{})
So(err, ShouldEqual, zerr.ErrBucketDoesNotExist)
})
Convey("UpdateUserAPIKey", func() {
err = boltdbWrapper.UpdateUserAPIKeyLastUsed(ctx, "")
So(err, ShouldNotBeNil)
userAc := reqCtx.NewUserAccessControl()
ctx := userAc.DeriveContext(context.Background())
err = boltdbWrapper.UpdateUserAPIKeyLastUsed(ctx, "") //nolint: contextcheck
So(err, ShouldNotBeNil)
})
Convey("DeleteUserAPIKey", func() {
err = boltdbWrapper.SetUserData(ctx, mTypes.UserData{})
So(err, ShouldBeNil)
err = boltdbWrapper.AddUserAPIKey(ctx, "hashedKey", &mTypes.APIKeyDetails{})
So(err, ShouldBeNil)
Convey("no such bucket", func() {
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserAPIKeysBucket))
})
So(err, ShouldBeNil)
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("test")
ctx := userAc.DeriveContext(context.Background())
err = boltdbWrapper.DeleteUserAPIKey(ctx, "")
So(err, ShouldEqual, zerr.ErrBucketDoesNotExist)
})
Convey("userdata not found", func() {
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("test")
ctx := userAc.DeriveContext(context.Background())
err := boltdbWrapper.DeleteUserData(ctx)
So(err, ShouldBeNil)
err = boltdbWrapper.DeleteUserAPIKey(ctx, "")
So(err, ShouldNotBeNil)
})
userAc := reqCtx.NewUserAccessControl()
ctx := userAc.DeriveContext(context.Background())
err = boltdbWrapper.DeleteUserAPIKey(ctx, "test") //nolint: contextcheck
So(err, ShouldNotBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserDataBucket))
})
So(err, ShouldBeNil)
err = boltdbWrapper.DeleteUserAPIKey(ctx, "") //nolint: contextcheck
So(err, ShouldNotBeNil)
})
Convey("GetUserAPIKeyInfo", func() {
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserAPIKeysBucket))
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetUserAPIKeyInfo("")
So(err, ShouldNotBeNil)
})
Convey("GetUserData", func() {
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
buck := tx.Bucket([]byte(boltdb.UserDataBucket))
So(buck, ShouldNotBeNil)
return buck.Put([]byte("test"), []byte("dsa8"))
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetUserData(ctx)
So(err, ShouldNotBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserAPIKeysBucket))
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.GetUserData(ctx)
So(err, ShouldNotBeNil)
})
Convey("SetUserData", func() {
userAc := reqCtx.NewUserAccessControl()
ctx := userAc.DeriveContext(context.Background())
err = boltdbWrapper.SetUserData(ctx, mTypes.UserData{})
So(err, ShouldNotBeNil)
buff := make([]byte, int(math.Ceil(float64(1000000)/float64(1.33333333333))))
_, err := rand.Read(buff)
So(err, ShouldBeNil)
longString := base64.RawURLEncoding.EncodeToString(buff)
userAc = reqCtx.NewUserAccessControl()
userAc.SetUsername(longString)
ctx = userAc.DeriveContext(context.Background())
err = boltdbWrapper.SetUserData(ctx, mTypes.UserData{}) //nolint: contextcheck
So(err, ShouldNotBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserDataBucket))
})
So(err, ShouldBeNil)
userAc = reqCtx.NewUserAccessControl()
userAc.SetUsername("test")
ctx = userAc.DeriveContext(context.Background())
err = boltdbWrapper.SetUserData(ctx, mTypes.UserData{}) //nolint: contextcheck
So(err, ShouldNotBeNil)
})
Convey("DeleteUserData", func() {
userAc = reqCtx.NewUserAccessControl()
ctx = userAc.DeriveContext(context.Background()) //nolint:fatcontext // test code
err = boltdbWrapper.DeleteUserData(ctx)
So(err, ShouldNotBeNil)
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
return tx.DeleteBucket([]byte(boltdb.UserDataBucket))
})
So(err, ShouldBeNil)
userAc = reqCtx.NewUserAccessControl()
userAc.SetUsername("test")
ctx = userAc.DeriveContext(context.Background())
err = boltdbWrapper.DeleteUserData(ctx)
So(err, ShouldNotBeNil)
})
Convey("GetUserGroups and SetUserGroups", func() {
userAc = reqCtx.NewUserAccessControl()
ctx = userAc.DeriveContext(context.Background()) //nolint:fatcontext // test code
_, err := boltdbWrapper.GetUserGroups(ctx)
So(err, ShouldNotBeNil)
err = boltdbWrapper.SetUserGroups(ctx, []string{})
So(err, ShouldNotBeNil)
})
Convey("ToggleStarRepo bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
_, err := boltdbWrapper.ToggleStarRepo(ctx, "repo")
So(err, ShouldNotBeNil)
})
Convey("ToggleStarRepo, no repoMeta found", func() {
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("username")
userAc.SetGlobPatterns("read", map[string]bool{
"repo": true,
})
ctx := userAc.DeriveContext(context.Background())
err := boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoBuck := tx.Bucket([]byte(boltdb.RepoMetaBuck))
err := repoBuck.Put([]byte("repo"), []byte("bad repo"))
So(err, ShouldBeNil)
return nil
})
So(err, ShouldBeNil)
_, err = boltdbWrapper.ToggleStarRepo(ctx, "repo")
So(err, ShouldNotBeNil)
})
Convey("ToggleStarRepo, bad repoMeta found", func() {
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("username")
userAc.SetGlobPatterns("read", map[string]bool{
"repo": true,
})
ctx := userAc.DeriveContext(context.Background())
_, err = boltdbWrapper.ToggleStarRepo(ctx, "repo")
So(err, ShouldNotBeNil)
})
Convey("ToggleBookmarkRepo bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
_, err := boltdbWrapper.ToggleBookmarkRepo(ctx, "repo")
So(err, ShouldNotBeNil)
})
Convey("GetUserData bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
_, err := boltdbWrapper.GetUserData(ctx)
So(err, ShouldNotBeNil)
})
Convey("SetUserData bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
err := boltdbWrapper.SetUserData(ctx, mTypes.UserData{})
So(err, ShouldNotBeNil)
})
Convey("GetUserGroups bad context errors", func() {
_, err := boltdbWrapper.GetUserGroups(ctx)
So(err, ShouldNotBeNil)
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
_, err = boltdbWrapper.GetUserGroups(ctx) //nolint: contextcheck
So(err, ShouldNotBeNil)
})
Convey("SetUserGroups bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
err := boltdbWrapper.SetUserGroups(ctx, []string{})
So(err, ShouldNotBeNil)
})
Convey("AddUserAPIKey bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
err := boltdbWrapper.AddUserAPIKey(ctx, "", &mTypes.APIKeyDetails{})
So(err, ShouldNotBeNil)
})
Convey("DeleteUserAPIKey bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
err := boltdbWrapper.DeleteUserAPIKey(ctx, "")
So(err, ShouldNotBeNil)
})
Convey("UpdateUserAPIKeyLastUsed bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
err := boltdbWrapper.UpdateUserAPIKeyLastUsed(ctx, "")
So(err, ShouldNotBeNil)
})
Convey("DeleteUserData bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
err := boltdbWrapper.DeleteUserData(ctx)
So(err, ShouldNotBeNil)
})
Convey("GetStarredRepos bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
_, err := boltdbWrapper.GetStarredRepos(ctx)
So(err, ShouldNotBeNil)
})
Convey("GetBookmarkedRepos bad context errors", func() {
uacKey := reqCtx.GetContextKey()
ctx := context.WithValue(context.Background(), uacKey, "bad context")
_, err := boltdbWrapper.GetBookmarkedRepos(ctx)
So(err, ShouldNotBeNil)
})
Convey("GetUserRepoMeta unmarshal error", func() {
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("username")
userAc.SetGlobPatterns("read", map[string]bool{
"repo": true,
})
ctx := userAc.DeriveContext(context.Background())
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
repoBuck := tx.Bucket([]byte(boltdb.RepoMetaBuck))
err := repoBuck.Put([]byte("repo"), []byte("bad repo"))
So(err, ShouldBeNil)
return nil
})
So(err, ShouldBeNil)
_, err := boltdbWrapper.GetRepoMeta(ctx, "repo")
So(err, ShouldNotBeNil)
})
})
}
func setRepoMeta(repo string, blob []byte, db *bbolt.DB) error { //nolint: unparam
err := db.Update(func(tx *bbolt.Tx) error {
buck := tx.Bucket([]byte(boltdb.RepoMetaBuck))
return buck.Put([]byte(repo), blob)
})
return err
}
func setImageMeta(digest godigest.Digest, blob []byte, db *bbolt.DB) error {
err := db.Update(func(tx *bbolt.Tx) error {
buck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
return buck.Put([]byte(digest.String()), blob)
})
return err
}
func setRepoBlobInfo(repo string, blob []byte, db *bbolt.DB) error {
err := db.Update(func(tx *bbolt.Tx) error {
buck := tx.Bucket([]byte(boltdb.RepoBlobsBuck))
return buck.Put([]byte(repo), blob)
})
return err
}
func TestBoltDBCountRepos(t *testing.T) {
Convey("CountRepos", t, func() {
tmpDir := t.TempDir()
boltDBParams := boltdb.DBParameters{RootDir: tmpDir}
boltDriver, err := boltdb.GetBoltDriver(boltDBParams)
So(err, ShouldBeNil)
log := log.NewTestLogger()
boltdbWrapper, err := boltdb.New(boltDriver, log)
So(boltdbWrapper, ShouldNotBeNil)
So(err, ShouldBeNil)
ctx := context.Background()
Convey("returns 0 on empty DB", func() {
count, err := boltdbWrapper.CountRepos(ctx)
So(err, ShouldBeNil)
So(count, ShouldEqual, 0)
})
Convey("returns correct count after adding repos", func() {
for i := range 3 {
err := boltdbWrapper.SetRepoMeta(fmt.Sprintf("repo%d", i), mTypes.RepoMeta{
Name: fmt.Sprintf("repo%d", i),
})
So(err, ShouldBeNil)
}
count, err := boltdbWrapper.CountRepos(ctx)
So(err, ShouldBeNil)
So(count, ShouldEqual, 3)
})
Convey("returns correct count after deleting a repo", func() {
for i := range 3 {
err := boltdbWrapper.SetRepoMeta(fmt.Sprintf("repo%d", i), mTypes.RepoMeta{
Name: fmt.Sprintf("repo%d", i),
})
So(err, ShouldBeNil)
}
err := boltdbWrapper.DeleteRepoMeta("repo1")
So(err, ShouldBeNil)
count, err := boltdbWrapper.CountRepos(ctx)
So(err, ShouldBeNil)
So(count, ShouldEqual, 2)
})
})
}
func TestBoltDBFastRestartStamp(t *testing.T) {
Convey("FastRestartStamp", t, func() {
tmpDir := t.TempDir()
boltDBParams := boltdb.DBParameters{RootDir: tmpDir}
boltDriver, err := boltdb.GetBoltDriver(boltDBParams)
So(err, ShouldBeNil)
boltdbWrapper, err := boltdb.New(boltDriver, log.NewTestLogger())
So(err, ShouldBeNil)
So(boltdbWrapper, ShouldNotBeNil)
Convey("returns empty before set", func() {
v, err := boltdbWrapper.GetFastRestartStamp()
So(err, ShouldBeNil)
So(v, ShouldEqual, "")
})
Convey("round-trips a value", func() {
So(boltdbWrapper.SetFastRestartStamp("v2.3.4"), ShouldBeNil)
v, err := boltdbWrapper.GetFastRestartStamp()
So(err, ShouldBeNil)
So(v, ShouldEqual, "v2.3.4")
})
Convey("overwrites a previous value", func() {
So(boltdbWrapper.SetFastRestartStamp("v1"), ShouldBeNil)
So(boltdbWrapper.SetFastRestartStamp("v2"), ShouldBeNil)
v, err := boltdbWrapper.GetFastRestartStamp()
So(err, ShouldBeNil)
So(v, ShouldEqual, "v2")
})
Convey("ResetDB clears the stamp", func() {
So(boltdbWrapper.SetFastRestartStamp("v1"), ShouldBeNil)
So(boltdbWrapper.ResetDB(), ShouldBeNil)
v, err := boltdbWrapper.GetFastRestartStamp()
So(err, ShouldBeNil)
So(v, ShouldEqual, "")
})
})
}