From 2f3c84bd1f86f1d005de23da50ce6348bd5e58f6 Mon Sep 17 00:00:00 2001 From: a Date: Fri, 3 Nov 2023 17:28:01 -0500 Subject: [PATCH 1/2] feat(sync): local tmp store Signed-off-by: a --- pkg/cli/server/root.go | 6 - pkg/extensions/config/sync/config.go | 1 + pkg/extensions/extension_sync.go | 39 +- .../sync/{local.go => destination.go} | 29 +- pkg/extensions/sync/service.go | 47 +- pkg/extensions/sync/sync.go | 2 +- pkg/extensions/sync/sync_internal_test.go | 12 +- test/blackbox/sync_remote.bats | 534 ++++++++++++++++++ 8 files changed, 615 insertions(+), 55 deletions(-) rename pkg/extensions/sync/{local.go => destination.go} (88%) create mode 100644 test/blackbox/sync_remote.bats diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index 5809ddad7..67e184593 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -392,12 +392,6 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error { return zerr.ErrBadConfig } - // enforce filesystem storage in case sync feature is enabled - if config.Extensions != nil && config.Extensions.Sync != nil { - log.Error().Err(zerr.ErrBadConfig).Msg("sync supports only filesystem storage") - - return zerr.ErrBadConfig - } } // enforce s3 driver on subpaths in case of using storage driver diff --git a/pkg/extensions/config/sync/config.go b/pkg/extensions/config/sync/config.go index 96c1361d4..eda4844b2 100644 --- a/pkg/extensions/config/sync/config.go +++ b/pkg/extensions/config/sync/config.go @@ -15,6 +15,7 @@ type Credentials struct { type Config struct { Enable *bool CredentialsFile string + TmpDir string Registries []RegistryConfig } diff --git a/pkg/extensions/extension_sync.go b/pkg/extensions/extension_sync.go index 852112dc0..a8f9c16f9 100644 --- a/pkg/extensions/extension_sync.go +++ b/pkg/extensions/extension_sync.go @@ -6,6 +6,7 @@ package extensions import ( "net" "net/url" + "os" "strings" zerr "zotregistry.io/zot/errors" @@ -41,23 +42,31 @@ func EnableSyncExtension(config *config.Config, metaDB mTypes.MetaDB, isPeriodical := len(registryConfig.Content) != 0 && registryConfig.PollInterval != 0 isOnDemand := registryConfig.OnDemand - if isPeriodical || isOnDemand { - service, err := sync.New(registryConfig, config.Extensions.Sync.CredentialsFile, - storeController, metaDB, log) - if err != nil { - return nil, err - } + if !(isPeriodical || isOnDemand) { + continue + } - if isPeriodical { - // add to task scheduler periodic sync - gen := sync.NewTaskGenerator(service, log) - sch.SubmitGenerator(gen, registryConfig.PollInterval, scheduler.MediumPriority) - } + tmpDir := config.Extensions.Sync.TmpDir + if tmpDir == "" { + // use an os tmpdir as tmpdir if not set + tmpDir = os.TempDir() + } - if isOnDemand { - // onDemand services used in routes.go - onDemand.Add(service) - } + service, err := sync.New(registryConfig, config.Extensions.Sync.CredentialsFile, tmpDir, + storeController, metaDB, log) + if err != nil { + return nil, err + } + + if isPeriodical { + // add to task scheduler periodic sync + gen := sync.NewTaskGenerator(service, log) + sch.SubmitGenerator(gen, registryConfig.PollInterval, scheduler.MediumPriority) + } + + if isOnDemand { + // onDemand services used in routes.go + onDemand.Add(service) } } diff --git a/pkg/extensions/sync/local.go b/pkg/extensions/sync/destination.go similarity index 88% rename from pkg/extensions/sync/local.go rename to pkg/extensions/sync/destination.go index 86ccf357c..36483d4cf 100644 --- a/pkg/extensions/sync/local.go +++ b/pkg/extensions/sync/destination.go @@ -29,25 +29,34 @@ import ( storageTypes "zotregistry.io/zot/pkg/storage/types" ) -type LocalRegistry struct { +type DestinationRegistry struct { storeController storage.StoreController tempStorage OciLayoutStorage metaDB mTypes.MetaDB log log.Logger } -func NewLocalRegistry(storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger) Local { - return &LocalRegistry{ +func NewDestinationRegistry( + storeController storage.StoreController, + tmpStorage OciLayoutStorage, + metaDB mTypes.MetaDB, + log log.Logger, +) Destination { + if tmpStorage == nil { + // to allow passing nil we can do this, noting that it will only work for a local StoreController + tmpStorage = NewOciLayoutStorage(storeController) + } + return &DestinationRegistry{ storeController: storeController, metaDB: metaDB, // first we sync from remote (using containers/image copy from docker:// to oci:) to a temp imageStore // then we copy the image from tempStorage to zot's storage using ImageStore APIs - tempStorage: NewOciLayoutStorage(storeController), + tempStorage: tmpStorage, log: log, } } -func (registry *LocalRegistry) CanSkipImage(repo, tag string, imageDigest digest.Digest) (bool, error) { +func (registry *DestinationRegistry) CanSkipImage(repo, tag string, imageDigest digest.Digest) (bool, error) { // check image already synced imageStore := registry.storeController.GetImageStore(repo) @@ -75,16 +84,16 @@ func (registry *LocalRegistry) CanSkipImage(repo, tag string, imageDigest digest return true, nil } -func (registry *LocalRegistry) GetContext() *types.SystemContext { +func (registry *DestinationRegistry) GetContext() *types.SystemContext { return registry.tempStorage.GetContext() } -func (registry *LocalRegistry) GetImageReference(repo, reference string) (types.ImageReference, error) { +func (registry *DestinationRegistry) GetImageReference(repo, reference string) (types.ImageReference, error) { return registry.tempStorage.GetImageReference(repo, reference) } // finalize a syncing image. -func (registry *LocalRegistry) CommitImage(imageReference types.ImageReference, repo, reference string) error { +func (registry *DestinationRegistry) CommitImage(imageReference types.ImageReference, repo, reference string) error { imageStore := registry.storeController.GetImageStore(repo) tempImageStore := getImageStoreFromImageReference(imageReference, repo, reference) @@ -180,7 +189,7 @@ func (registry *LocalRegistry) CommitImage(imageReference types.ImageReference, return nil } -func (registry *LocalRegistry) copyManifest(repo string, manifestContent []byte, reference string, +func (registry *DestinationRegistry) copyManifest(repo string, manifestContent []byte, reference string, tempImageStore storageTypes.ImageStore, ) error { imageStore := registry.storeController.GetImageStore(repo) @@ -239,7 +248,7 @@ func (registry *LocalRegistry) copyManifest(repo string, manifestContent []byte, } // Copy a blob from one image store to another image store. -func (registry *LocalRegistry) copyBlob(repo string, blobDigest digest.Digest, blobMediaType string, +func (registry *DestinationRegistry) copyBlob(repo string, blobDigest digest.Digest, blobMediaType string, tempImageStore storageTypes.ImageStore, ) error { imageStore := registry.storeController.GetImageStore(repo) diff --git a/pkg/extensions/sync/service.go b/pkg/extensions/sync/service.go index 725af0870..63b5f360e 100644 --- a/pkg/extensions/sync/service.go +++ b/pkg/extensions/sync/service.go @@ -20,13 +20,14 @@ import ( "zotregistry.io/zot/pkg/log" mTypes "zotregistry.io/zot/pkg/meta/types" "zotregistry.io/zot/pkg/storage" + "zotregistry.io/zot/pkg/storage/local" ) type BaseService struct { config syncconf.RegistryConfig credentials syncconf.CredentialsFile remote Remote - local Local + destination Destination retryOptions *retry.RetryOptions contentManager ContentManager storeController storage.StoreController @@ -37,8 +38,13 @@ type BaseService struct { log log.Logger } -func New(opts syncconf.RegistryConfig, credentialsFilepath string, - storeController storage.StoreController, metadb mTypes.MetaDB, log log.Logger, +func New( + opts syncconf.RegistryConfig, + credentialsFilepath string, + tmpDir string, + storeController storage.StoreController, + metadb mTypes.MetaDB, + log log.Logger, ) (Service, error) { service := &BaseService{} @@ -60,7 +66,14 @@ func New(opts syncconf.RegistryConfig, credentialsFilepath string, service.credentials = credentialsFile service.contentManager = NewContentManager(opts.Content, log) - service.local = NewLocalRegistry(storeController, metadb, log) + + tmpImageStore := local.NewImageStore(tmpDir, + false, false, log, nil, nil, nil, + ) + + tmpStorage := NewOciLayoutStorage(storage.StoreController{DefaultStore: tmpImageStore}) + + service.destination = NewDestinationRegistry(storeController, tmpStorage, metadb, log) retryOptions := &retry.RetryOptions{} @@ -289,7 +302,7 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error { service.log.Info().Str("repo", repo).Msgf("sync: syncing tags %v", tags) // apply content.destination rule - localRepo := service.contentManager.GetRepoDestination(repo) + destinationRepo := service.contentManager.GetRepoDestination(repo) for _, tag := range tags { if common.IsContextDone(ctx) { @@ -303,7 +316,7 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error { var manifestDigest digest.Digest if err = retry.RetryIfNecessary(ctx, func() error { - manifestDigest, err = service.syncTag(ctx, localRepo, repo, tag) + manifestDigest, err = service.syncTag(ctx, destinationRepo, repo, tag) return err }, service.retryOptions); err != nil { @@ -320,7 +333,7 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error { if manifestDigest != "" { if err = retry.RetryIfNecessary(ctx, func() error { - err = service.references.SyncAll(ctx, localRepo, repo, manifestDigest.String()) + err = service.references.SyncAll(ctx, destinationRepo, repo, manifestDigest.String()) if errors.Is(err, zerr.ErrSyncReferrerNotFound) { return nil } @@ -340,8 +353,8 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error { return nil } -func (service *BaseService) syncTag(ctx context.Context, localRepo, remoteRepo, tag string) (digest.Digest, error) { - copyOptions := getCopyOptions(service.remote.GetContext(), service.local.GetContext()) +func (service *BaseService) syncTag(ctx context.Context, destinationRepo, remoteRepo, tag string) (digest.Digest, error) { + copyOptions := getCopyOptions(service.remote.GetContext(), service.destination.GetContext()) policyContext, err := getPolicyContext(service.log) if err != nil { @@ -384,38 +397,38 @@ func (service *BaseService) syncTag(ctx context.Context, localRepo, remoteRepo, } } - skipImage, err := service.local.CanSkipImage(localRepo, tag, manifestDigest) + skipImage, err := service.destination.CanSkipImage(destinationRepo, tag, manifestDigest) if err != nil { service.log.Error().Err(err).Str("errortype", common.TypeOf(err)). - Str("repo", localRepo).Str("reference", tag). + Str("repo", destinationRepo).Str("reference", tag). Msg("couldn't check if the local image can be skipped") } if !skipImage { - localImageRef, err := service.local.GetImageReference(localRepo, tag) + localImageRef, err := service.destination.GetImageReference(destinationRepo, tag) if err != nil { service.log.Error().Err(err).Str("errortype", common.TypeOf(err)). - Str("repo", localRepo).Str("reference", tag).Msg("couldn't get a local image reference") + Str("repo", destinationRepo).Str("reference", tag).Msg("couldn't get a local image reference") return "", err } service.log.Info().Str("remote image", remoteImageRef.DockerReference().String()). - Str("local image", fmt.Sprintf("%s:%s", localRepo, tag)).Msg("syncing image") + Str("local image", fmt.Sprintf("%s:%s", destinationRepo, tag)).Msg("syncing image") _, err = copy.Image(ctx, policyContext, localImageRef, remoteImageRef, ©Options) if err != nil { service.log.Error().Err(err).Str("errortype", common.TypeOf(err)). Str("remote image", remoteImageRef.DockerReference().String()). - Str("local image", fmt.Sprintf("%s:%s", localRepo, tag)).Msg("coulnd't sync image") + Str("local image", fmt.Sprintf("%s:%s", destinationRepo, tag)).Msg("coulnd't sync image") return "", err } - err = service.local.CommitImage(localImageRef, localRepo, tag) + err = service.destination.CommitImage(localImageRef, destinationRepo, tag) if err != nil { service.log.Error().Err(err).Str("errortype", common.TypeOf(err)). - Str("repo", localRepo).Str("reference", tag).Msg("couldn't commit image to local image store") + Str("repo", destinationRepo).Str("reference", tag).Msg("couldn't commit image to local image store") return "", err } diff --git a/pkg/extensions/sync/sync.go b/pkg/extensions/sync/sync.go index bb4dd0bf4..c285c5726 100644 --- a/pkg/extensions/sync/sync.go +++ b/pkg/extensions/sync/sync.go @@ -65,7 +65,7 @@ type Remote interface { } // Local registry. -type Local interface { +type Destination interface { Registry // Check if an image is already synced CanSkipImage(repo, tag string, imageDigest digest.Digest) (bool, error) diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 4f025cd90..538318c0c 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -162,7 +162,7 @@ func TestService(t *testing.T) { URLs: []string{"http://localhost"}, } - service, err := New(conf, "", storage.StoreController{}, mocks.MetaDBMock{}, log.Logger{}) + service, err := New(conf, "", os.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.Logger{}) So(err, ShouldBeNil) err = service.SyncRepo(context.Background(), "repo") @@ -170,7 +170,7 @@ func TestService(t *testing.T) { }) } -func TestLocalRegistry(t *testing.T) { +func TestDestinationRegistry(t *testing.T) { Convey("make StoreController", t, func() { dir := t.TempDir() @@ -185,7 +185,7 @@ func TestLocalRegistry(t *testing.T) { syncImgStore := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver) repoName := "repo" - registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, log) + registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, nil, log) imageReference, err := registry.GetImageReference(repoName, "1.0") So(err, ShouldBeNil) So(imageReference, ShouldNotBeNil) @@ -302,7 +302,7 @@ func TestLocalRegistry(t *testing.T) { syncImgStore := local.NewImageStore(dir, true, true, log, metrics, linter, cacheDriver) repoName := "repo" - registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, log) + registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, nil, log) err = registry.CommitImage(imageReference, repoName, "1.0") So(err, ShouldBeNil) @@ -336,7 +336,7 @@ func TestLocalRegistry(t *testing.T) { }) Convey("trigger metaDB error on index manifest in CommitImage()", func() { - registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{ + registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, mocks.MetaDBMock{ SetRepoReferenceFn: func(ctx context.Context, repo string, reference string, imageMeta mTypes.ImageMeta) error { if reference == "1.0" { return zerr.ErrRepoMetaNotFound @@ -351,7 +351,7 @@ func TestLocalRegistry(t *testing.T) { }) Convey("trigger metaDB error on image manifest in CommitImage()", func() { - registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{ + registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, mocks.MetaDBMock{ SetRepoReferenceFn: func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error { return zerr.ErrRepoMetaNotFound }, diff --git a/test/blackbox/sync_remote.bats b/test/blackbox/sync_remote.bats new file mode 100644 index 000000000..6a1ee2026 --- /dev/null +++ b/test/blackbox/sync_remote.bats @@ -0,0 +1,534 @@ +# Note: Intended to be run as "make run-blackbox-tests" or "make run-blackbox-ci" +# Makefile target installs & checks all necessary tooling +# Extra tools that are not covered in Makefile target needs to be added in verify_prerequisites() + +load helpers_zot +load helpers_wait +load helpers_cloud + + +function verify_prerequisites() { + if [ ! $(command -v curl) ]; then + echo "you need to install curl as a prerequisite to running the tests" >&3 + return 1 + fi + + if [ ! $(command -v jq) ]; then + echo "you need to install jq as a prerequisite to running the tests" >&3 + return 1 + fi + + return 0 +} + +function setup_file() { + export COSIGN_PASSWORD="" + export COSIGN_OCI_EXPERIMENTAL=1 + export COSIGN_EXPERIMENTAL=1 + + # Verify prerequisites are available + if ! $(verify_prerequisites); then + exit 1 + fi + + # Download test data to folder common for the entire suite, not just this file + skopeo --insecure-policy copy --format=oci docker://ghcr.io/project-zot/golang:1.20 oci:${TEST_DATA_DIR}/golang:1.20 + + + # Setup zot server + local zot_sync_per_root_dir=${BATS_FILE_TMPDIR}/zot-per + local zot_sync_ondemand_root_dir=${BATS_FILE_TMPDIR}/zot-ondemand + + local zot_sync_per_config_file=${BATS_FILE_TMPDIR}/zot_sync_per_config.json + local zot_sync_ondemand_config_file=${BATS_FILE_TMPDIR}/zot_sync_ondemand_config.json + + local zot_minimal_root_dir=${BATS_FILE_TMPDIR}/zot-minimal + local zot_minimal_config_file=${BATS_FILE_TMPDIR}/zot_minimal_config.json + + local oci_data_dir=${BATS_FILE_TMPDIR}/oci + mkdir -p ${zot_sync_per_root_dir} + mkdir -p ${zot_sync_ondemand_root_dir} + mkdir -p ${zot_minimal_root_dir} + mkdir -p ${oci_data_dir} + + cat >${zot_sync_per_config_file} <${zot_sync_ondemand_config_file} <${zot_minimal_config_file} <${trust_policy_file} < config.json +echo "hello world" > artifact.txt +run oras push --plain-http 127.0.0.1:9000/hello-artifact:v2 \ + --config config.json:application/vnd.acme.rocket.config.v1+json artifact.txt:text/plain -d -v + [ "$status" -eq 0 ] + rm -f artifact.txt + rm -f config.json +} + +@test "sync oras artifact periodically" { +# # wait for oras artifact to be copied +run sleep 15s +run oras pull --plain-http 127.0.0.1:8081/hello-artifact:v2 -d -v +[ "$status" -eq 0 ] +grep -q "hello world" artifact.txt +rm -f artifact.txt +} + +@test "sync oras artifact on demand" { +run oras pull --plain-http 127.0.0.1:8082/hello-artifact:v2 -d -v +[ "$status" -eq 0 ] +grep -q "hello world" artifact.txt +rm -f artifact.txt +} + +# sync helm chart +@test "push helm chart" { +run helm package ${BATS_FILE_TMPDIR}/helm-charts/charts/zot -d ${BATS_FILE_TMPDIR} +[ "$status" -eq 0 ] +local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) +run helm push ${BATS_FILE_TMPDIR}/zot-${chart_version}.tgz oci://localhost:9000/zot-chart +[ "$status" -eq 0 ] +} + +@test "sync helm chart periodically" { +# wait for helm chart to be copied +run sleep 15s + +local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) +run helm pull oci://localhost:8081/zot-chart/zot --version ${chart_version} -d ${BATS_FILE_TMPDIR} +[ "$status" -eq 0 ] +} + +@test "sync helm chart on demand" { +local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) +run helm pull oci://localhost:8082/zot-chart/zot --version ${chart_version} -d ${BATS_FILE_TMPDIR} +[ "$status" -eq 0 ] +} + +# sync OCI artifacts +@test "push OCI artifact (oci image mediatype) with regclient" { +run regctl registry set localhost:9000 --tls disabled +run regctl registry set localhost:8081 --tls disabled +run regctl registry set localhost:8082 --tls disabled + +run regctl artifact put localhost:9000/artifact:demo < Date: Tue, 21 Nov 2023 19:32:14 +0200 Subject: [PATCH 2/2] fix(sync): various fixes for s3+remote storage feature Signed-off-by: Petu Eusebiu --- Makefile | 1 + examples/config-sync-cloud-storage.json | 49 ++ pkg/cli/server/extensions_test.go | 204 ++++++++ pkg/cli/server/root.go | 15 + pkg/extensions/config/sync/config.go | 7 +- pkg/extensions/extension_sync.go | 11 +- pkg/extensions/sync/destination.go | 20 +- pkg/extensions/sync/service.go | 27 +- pkg/extensions/sync/sync_internal_test.go | 12 +- test/blackbox/sync.bats | 6 +- test/blackbox/sync_cloud.bats | 571 ++++++++++++++++++++++ test/blackbox/sync_remote.bats | 534 -------------------- 12 files changed, 885 insertions(+), 572 deletions(-) create mode 100644 examples/config-sync-cloud-storage.json create mode 100644 test/blackbox/sync_cloud.bats delete mode 100644 test/blackbox/sync_remote.bats diff --git a/Makefile b/Makefile index 07ce9c1c6..f7872de3d 100644 --- a/Makefile +++ b/Makefile @@ -493,6 +493,7 @@ run-blackbox-ci: check-blackbox-prerequisites binary binary-minimal cli run-blackbox-cloud-ci: check-blackbox-prerequisites check-awslocal binary $(BATS) echo running cloud CI bats tests; \ $(BATS) $(BATS_FLAGS) test/blackbox/cloud_only.bats + $(BATS) $(BATS_FLAGS) test/blackbox/sync_cloud.bats .PHONY: run-blackbox-dedupe-nightly run-blackbox-dedupe-nightly: check-blackbox-prerequisites check-awslocal binary binary-minimal diff --git a/examples/config-sync-cloud-storage.json b/examples/config-sync-cloud-storage.json new file mode 100644 index 000000000..bd4fbbb60 --- /dev/null +++ b/examples/config-sync-cloud-storage.json @@ -0,0 +1,49 @@ +{ + "distSpecVersion": "1.1.0-dev", + "storage": { + "rootDirectory": "/tmp/zot", + "dedupe": true, + "gc": true, + "remoteCache": true, + "storageDriver": { + "name": "s3", + "rootdirectory": "/zot", + "region": "us-east-2", + "bucket": "zot-storage", + "secure": true, + "skipverify": false + }, + "cacheDriver": { + "name": "dynamodb", + "region": "us-east-2", + "cacheTablename": "BlobTable" + } + }, + "http": { + "address": "0.0.0.0", + "port": "8080" + }, + "log": { + "level": "debug" + }, + "extensions": { + "sync": { + "downloadDir": "/tmp/sync", + "registries": [ + { + "urls": [ + "http://localhost:5000" + ], + "onDemand": false, + "tlsVerify": false, + "PollInterval": "30m", + "content": [ + { + "prefix": "**" + } + ] + } + ] + } + } +} diff --git a/pkg/cli/server/extensions_test.go b/pkg/cli/server/extensions_test.go index 727e96a71..526fdc4dc 100644 --- a/pkg/cli/server/extensions_test.go +++ b/pkg/cli/server/extensions_test.go @@ -1653,3 +1653,207 @@ func TestOverlappingSyncRetentionConfig(t *testing.T) { So(string(data), ShouldContainSubstring, "overlapping sync content\":{\"Prefix\":\"prod/*") }) } + +func TestSyncWithRemoteStorageConfig(t *testing.T) { + oldArgs := os.Args + + defer func() { os.Args = oldArgs }() + + Convey("Test verify sync with remote storage works if sync.tmpdir is provided", t, func(c C) { + tmpfile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + + content := `{ + "distSpecVersion": "1.1.0-dev", + "storage": { + "rootDirectory": "%s", + "dedupe": false, + "remoteCache": false, + "storageDriver": { + "name": "s3", + "rootdirectory": "/zot", + "region": "us-east-2", + "regionendpoint": "localhost:4566", + "bucket": "zot-storage", + "secure": false, + "skipverify": false + } + }, + "http": { + "address": "0.0.0.0", + "port": "%s" + }, + "log": { + "level": "debug", + "output": "%s" + }, + "extensions": { + "sync": { + "downloadDir": "/tmp/sync", + "registries": [ + { + "urls": [ + "http://localhost:9000" + ], + "onDemand": true, + "tlsVerify": false, + "content": [ + { + "prefix": "**" + } + ] + } + ] + } + } + }` + + logPath, err := runCLIWithConfig(t.TempDir(), content) + So(err, ShouldBeNil) + + data, err := os.ReadFile(logPath) + So(err, ShouldBeNil) + defer os.Remove(logPath) // clean up + So(string(data), ShouldNotContainSubstring, + "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified") + }) + + Convey("Test verify sync with remote storage panics if sync.tmpdir is not provided", t, func(c C) { + port := GetFreePort() + logFile, err := os.CreateTemp("", "zot-log*.txt") + So(err, ShouldBeNil) + defer os.Remove(logFile.Name()) // clean up + + tmpfile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := fmt.Sprintf(`{ + "distSpecVersion": "1.1.0-dev", + "storage": { + "rootDirectory": "%s", + "dedupe": false, + "remoteCache": false, + "storageDriver": { + "name": "s3", + "rootdirectory": "/zot", + "region": "us-east-2", + "regionendpoint": "localhost:4566", + "bucket": "zot-storage", + "secure": false, + "skipverify": false + } + }, + "http": { + "address": "0.0.0.0", + "port": "%s" + }, + "log": { + "level": "debug", + "output": "%s" + }, + "extensions": { + "sync": { + "registries": [ + { + "urls": [ + "http://localhost:9000" + ], + "onDemand": true, + "tlsVerify": false, + "content": [ + { + "prefix": "**" + } + ] + } + ] + } + } + }`, t.TempDir(), port, logFile.Name()) + + err = os.WriteFile(tmpfile.Name(), []byte(content), 0o0600) + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "serve", tmpfile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldNotBeNil) + + data, err := os.ReadFile(logFile.Name()) + So(err, ShouldBeNil) + defer os.Remove(logFile.Name()) // clean up + So(string(data), ShouldContainSubstring, + "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified") + }) + + Convey("Test verify sync with remote storage on subpath panics if sync.tmpdir is not provided", t, func(c C) { + port := GetFreePort() + logFile, err := os.CreateTemp("", "zot-log*.txt") + So(err, ShouldBeNil) + defer os.Remove(logFile.Name()) // clean up + + tmpfile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := fmt.Sprintf(`{ + "distSpecVersion": "1.1.0-dev", + "storage": { + "rootDirectory": "%s", + "subPaths":{ + "/a": { + "rootDirectory": "%s", + "dedupe": false, + "remoteCache": false, + "storageDriver":{ + "name":"s3", + "rootdirectory":"/zot-a", + "region":"us-east-2", + "bucket":"zot-storage", + "secure":true, + "skipverify":true + } + } + } + }, + "http": { + "address": "0.0.0.0", + "port": "%s" + }, + "log": { + "level": "debug", + "output": "%s" + }, + "extensions": { + "sync": { + "registries": [ + { + "urls": [ + "http://localhost:9000" + ], + "onDemand": true, + "tlsVerify": false, + "content": [ + { + "prefix": "**" + } + ] + } + ] + } + } + }`, t.TempDir(), t.TempDir(), port, logFile.Name()) + + err = os.WriteFile(tmpfile.Name(), []byte(content), 0o0600) + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "serve", tmpfile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldNotBeNil) + + data, err := os.ReadFile(logFile.Name()) + So(err, ShouldBeNil) + defer os.Remove(logFile.Name()) // clean up + So(string(data), ShouldContainSubstring, + "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified") + }) +} diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index 67e184593..a73ef3573 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -392,6 +392,13 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error { return zerr.ErrBadConfig } + // enforce tmpDir in case sync + s3 + if config.Extensions != nil && config.Extensions.Sync != nil && config.Extensions.Sync.DownloadDir == "" { + log.Error().Err(zerr.ErrBadConfig). + Msg("using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified") + + return zerr.ErrBadConfig + } } // enforce s3 driver on subpaths in case of using storage driver @@ -407,6 +414,14 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error { return zerr.ErrBadConfig } + + // enforce tmpDir in case sync + s3 + if config.Extensions != nil && config.Extensions.Sync != nil && config.Extensions.Sync.DownloadDir == "" { + log.Error().Err(zerr.ErrBadConfig). + Msg("using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified") + + return zerr.ErrBadConfig + } } } } diff --git a/pkg/extensions/config/sync/config.go b/pkg/extensions/config/sync/config.go index eda4844b2..ec888a084 100644 --- a/pkg/extensions/config/sync/config.go +++ b/pkg/extensions/config/sync/config.go @@ -15,8 +15,11 @@ type Credentials struct { type Config struct { Enable *bool CredentialsFile string - TmpDir string - Registries []RegistryConfig + /* DownloadDir is needed only in case of using cloud based storages + it uses regclient to first copy images into this dir (as oci layout) + and then move them into storage. */ + DownloadDir string + Registries []RegistryConfig } type RegistryConfig struct { diff --git a/pkg/extensions/extension_sync.go b/pkg/extensions/extension_sync.go index a8f9c16f9..8c6c75baf 100644 --- a/pkg/extensions/extension_sync.go +++ b/pkg/extensions/extension_sync.go @@ -6,7 +6,6 @@ package extensions import ( "net" "net/url" - "os" "strings" zerr "zotregistry.io/zot/errors" @@ -46,14 +45,10 @@ func EnableSyncExtension(config *config.Config, metaDB mTypes.MetaDB, continue } - tmpDir := config.Extensions.Sync.TmpDir - if tmpDir == "" { - // use an os tmpdir as tmpdir if not set - tmpDir = os.TempDir() - } + tmpDir := config.Extensions.Sync.DownloadDir + credsPath := config.Extensions.Sync.CredentialsFile - service, err := sync.New(registryConfig, config.Extensions.Sync.CredentialsFile, tmpDir, - storeController, metaDB, log) + service, err := sync.New(registryConfig, credsPath, tmpDir, storeController, metaDB, log) if err != nil { return nil, err } diff --git a/pkg/extensions/sync/destination.go b/pkg/extensions/sync/destination.go index 36483d4cf..5150bc757 100644 --- a/pkg/extensions/sync/destination.go +++ b/pkg/extensions/sync/destination.go @@ -37,22 +37,18 @@ type DestinationRegistry struct { } func NewDestinationRegistry( - storeController storage.StoreController, - tmpStorage OciLayoutStorage, + storeController storage.StoreController, // local store controller + tempStoreController storage.StoreController, // temp store controller metaDB mTypes.MetaDB, log log.Logger, ) Destination { - if tmpStorage == nil { - // to allow passing nil we can do this, noting that it will only work for a local StoreController - tmpStorage = NewOciLayoutStorage(storeController) - } return &DestinationRegistry{ storeController: storeController, + tempStorage: NewOciLayoutStorage(tempStoreController), metaDB: metaDB, // first we sync from remote (using containers/image copy from docker:// to oci:) to a temp imageStore // then we copy the image from tempStorage to zot's storage using ImageStore APIs - tempStorage: tmpStorage, - log: log, + log: log, } } @@ -288,9 +284,11 @@ func getImageStoreFromImageReference(imageReference types.ImageReference, repo, tempRootDir = strings.ReplaceAll(imageReference.StringWithinTransport(), fmt.Sprintf("%s:", repo), "") } - metrics := monitoring.NewMetricsServer(false, log.Logger{}) + return getImageStore(tempRootDir) +} - tempImageStore := local.NewImageStore(tempRootDir, false, false, log.Logger{}, metrics, nil, nil) +func getImageStore(rootDir string) storageTypes.ImageStore { + metrics := monitoring.NewMetricsServer(false, log.Logger{}) - return tempImageStore + return local.NewImageStore(rootDir, false, false, log.Logger{}, metrics, nil, nil) } diff --git a/pkg/extensions/sync/service.go b/pkg/extensions/sync/service.go index 63b5f360e..d8b4add3f 100644 --- a/pkg/extensions/sync/service.go +++ b/pkg/extensions/sync/service.go @@ -20,7 +20,6 @@ import ( "zotregistry.io/zot/pkg/log" mTypes "zotregistry.io/zot/pkg/meta/types" "zotregistry.io/zot/pkg/storage" - "zotregistry.io/zot/pkg/storage/local" ) type BaseService struct { @@ -67,13 +66,20 @@ func New( service.contentManager = NewContentManager(opts.Content, log) - tmpImageStore := local.NewImageStore(tmpDir, - false, false, log, nil, nil, nil, - ) - - tmpStorage := NewOciLayoutStorage(storage.StoreController{DefaultStore: tmpImageStore}) - - service.destination = NewDestinationRegistry(storeController, tmpStorage, metadb, log) + if len(tmpDir) == 0 { + // first it will sync in tmpDir then it will move everything into local ImageStore + service.destination = NewDestinationRegistry(storeController, storeController, metadb, log) + } else { + // first it will sync under /rootDir/reponame/.sync/ then it will move everything into local ImageStore + service.destination = NewDestinationRegistry( + storeController, + storage.StoreController{ + DefaultStore: getImageStore(tmpDir), + }, + metadb, + log, + ) + } retryOptions := &retry.RetryOptions{} @@ -140,7 +146,7 @@ func (service *BaseService) SetNextAvailableClient() error { if err != nil { service.log.Error().Err(err).Str("url", url).Msg("sync: failed to initialize http client") - continue + return err } if !service.client.Ping() { @@ -353,7 +359,8 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error { return nil } -func (service *BaseService) syncTag(ctx context.Context, destinationRepo, remoteRepo, tag string) (digest.Digest, error) { +func (service *BaseService) syncTag(ctx context.Context, destinationRepo, remoteRepo, tag string, +) (digest.Digest, error) { copyOptions := getCopyOptions(service.remote.GetContext(), service.destination.GetContext()) policyContext, err := getPolicyContext(service.log) diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 538318c0c..50c572ca1 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -185,7 +185,8 @@ func TestDestinationRegistry(t *testing.T) { syncImgStore := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver) repoName := "repo" - registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, nil, log) + storeController := storage.StoreController{DefaultStore: syncImgStore} + registry := NewDestinationRegistry(storeController, storeController, nil, log) imageReference, err := registry.GetImageReference(repoName, "1.0") So(err, ShouldBeNil) So(imageReference, ShouldNotBeNil) @@ -302,7 +303,8 @@ func TestDestinationRegistry(t *testing.T) { syncImgStore := local.NewImageStore(dir, true, true, log, metrics, linter, cacheDriver) repoName := "repo" - registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, nil, log) + storeController := storage.StoreController{DefaultStore: syncImgStore} + registry := NewDestinationRegistry(storeController, storeController, nil, log) err = registry.CommitImage(imageReference, repoName, "1.0") So(err, ShouldBeNil) @@ -336,7 +338,8 @@ func TestDestinationRegistry(t *testing.T) { }) Convey("trigger metaDB error on index manifest in CommitImage()", func() { - registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, mocks.MetaDBMock{ + storeController := storage.StoreController{DefaultStore: syncImgStore} + registry := NewDestinationRegistry(storeController, storeController, mocks.MetaDBMock{ SetRepoReferenceFn: func(ctx context.Context, repo string, reference string, imageMeta mTypes.ImageMeta) error { if reference == "1.0" { return zerr.ErrRepoMetaNotFound @@ -351,7 +354,8 @@ func TestDestinationRegistry(t *testing.T) { }) Convey("trigger metaDB error on image manifest in CommitImage()", func() { - registry := NewDestinationRegistry(storage.StoreController{DefaultStore: syncImgStore}, nil, mocks.MetaDBMock{ + storeController := storage.StoreController{DefaultStore: syncImgStore} + registry := NewDestinationRegistry(storeController, storeController, mocks.MetaDBMock{ SetRepoReferenceFn: func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error { return zerr.ErrRepoMetaNotFound }, diff --git a/test/blackbox/sync.bats b/test/blackbox/sync.bats index 36b14722e..6ec24f70c 100644 --- a/test/blackbox/sync.bats +++ b/test/blackbox/sync.bats @@ -139,14 +139,14 @@ EOF EOF git -C ${BATS_FILE_TMPDIR} clone https://github.com/project-zot/helm-charts.git + zot_serve ${ZOT_MINIMAL_PATH} ${zot_minimal_config_file} + wait_zot_reachable ${zot_port3} + zot_serve ${ZOT_PATH} ${zot_sync_per_config_file} wait_zot_reachable ${zot_port1} zot_serve ${ZOT_PATH} ${zot_sync_ondemand_config_file} wait_zot_reachable ${zot_port2} - - zot_serve ${ZOT_MINIMAL_PATH} ${zot_minimal_config_file} - wait_zot_reachable ${zot_port3} } function teardown_file() { diff --git a/test/blackbox/sync_cloud.bats b/test/blackbox/sync_cloud.bats new file mode 100644 index 000000000..a35e76700 --- /dev/null +++ b/test/blackbox/sync_cloud.bats @@ -0,0 +1,571 @@ +# Note: Intended to be run as "make run-blackbox-tests" or "make run-blackbox-ci" +# Makefile target installs & checks all necessary tooling +# Extra tools that are not covered in Makefile target needs to be added in verify_prerequisites() + +load helpers_zot +load helpers_wait + + +function verify_prerequisites() { + if [ ! $(command -v curl) ]; then + echo "you need to install curl as a prerequisite to running the tests" >&3 + return 1 + fi + + if [ ! $(command -v jq) ]; then + echo "you need to install jq as a prerequisite to running the tests" >&3 + return 1 + fi + + return 0 +} + +function setup_file() { + export COSIGN_PASSWORD="" + export COSIGN_OCI_EXPERIMENTAL=1 + export COSIGN_EXPERIMENTAL=1 + + # Verify prerequisites are available + if ! $(verify_prerequisites); then + exit 1 + fi + + # Download test data to folder common for the entire suite, not just this file + skopeo --insecure-policy copy --format=oci docker://ghcr.io/project-zot/golang:1.20 oci:${TEST_DATA_DIR}/golang:1.20 + # Setup zot server + local zot_sync_per_root_dir=${BATS_FILE_TMPDIR}/zot-per + local zot_sync_ondemand_root_dir=${BATS_FILE_TMPDIR}/zot-ondemand + + local zot_sync_per_config_file=${BATS_FILE_TMPDIR}/zot_sync_per_config.json + local zot_sync_ondemand_config_file=${BATS_FILE_TMPDIR}/zot_sync_ondemand_config.json + + local zot_minimal_root_dir=${BATS_FILE_TMPDIR}/zot-minimal + local zot_minimal_config_file=${BATS_FILE_TMPDIR}/zot_minimal_config.json + + local oci_data_dir=${BATS_FILE_TMPDIR}/oci + mkdir -p ${zot_sync_per_root_dir} + mkdir -p ${zot_sync_ondemand_root_dir} + mkdir -p ${zot_minimal_root_dir} + mkdir -p ${oci_data_dir} + zot_port1=$(get_free_port) + echo ${zot_port1} > ${BATS_FILE_TMPDIR}/zot.port1 + zot_port2=$(get_free_port) + echo ${zot_port2} > ${BATS_FILE_TMPDIR}/zot.port2 + zot_port3=$(get_free_port) + echo ${zot_port3} > ${BATS_FILE_TMPDIR}/zot.port3 + + cat >${zot_sync_per_config_file} <${zot_sync_ondemand_config_file} <${zot_minimal_config_file} <${trust_policy_file} < config.json + echo "hello world" > artifact.txt + run oras push --plain-http 127.0.0.1:${zot_port3}/hello-artifact:v2 \ + --config config.json:application/vnd.acme.rocket.config.v1+json artifact.txt:text/plain -d -v + [ "$status" -eq 0 ] + rm -f artifact.txt + rm -f config.json +} + +@test "sync oras artifact periodically" { + zot_port1=`cat ${BATS_FILE_TMPDIR}/zot.port1` + # wait for oras artifact to be copied + run sleep 15s + run oras pull --plain-http 127.0.0.1:${zot_port1}/hello-artifact:v2 -d -v + [ "$status" -eq 0 ] + grep -q "hello world" artifact.txt + rm -f artifact.txt +} + +@test "sync oras artifact on demand" { + zot_port2=`cat ${BATS_FILE_TMPDIR}/zot.port2` + run oras pull --plain-http 127.0.0.1:${zot_port2}/hello-artifact:v2 -d -v + [ "$status" -eq 0 ] + grep -q "hello world" artifact.txt + rm -f artifact.txt +} + +# sync helm chart +@test "push helm chart" { + zot_port3=`cat ${BATS_FILE_TMPDIR}/zot.port3` + run helm package ${BATS_FILE_TMPDIR}/helm-charts/charts/zot -d ${BATS_FILE_TMPDIR} + [ "$status" -eq 0 ] + local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) + run helm push ${BATS_FILE_TMPDIR}/zot-${chart_version}.tgz oci://localhost:${zot_port3}/zot-chart + [ "$status" -eq 0 ] +} + +@test "sync helm chart periodically" { + zot_port1=`cat ${BATS_FILE_TMPDIR}/zot.port1` + # wait for helm chart to be copied + run sleep 15s + + local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) + run helm pull oci://localhost:${zot_port1}/zot-chart/zot --version ${chart_version} -d ${BATS_FILE_TMPDIR} + [ "$status" -eq 0 ] +} + +@test "sync helm chart on demand" { + zot_port2=`cat ${BATS_FILE_TMPDIR}/zot.port2` + local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) + run helm pull oci://localhost:${zot_port2}/zot-chart/zot --version ${chart_version} -d ${BATS_FILE_TMPDIR} + [ "$status" -eq 0 ] +} + +# sync OCI artifacts +@test "push OCI artifact (oci image mediatype) with regclient" { + zot_port1=`cat ${BATS_FILE_TMPDIR}/zot.port1` + zot_port2=`cat ${BATS_FILE_TMPDIR}/zot.port2` + zot_port3=`cat ${BATS_FILE_TMPDIR}/zot.port3` + run regctl registry set localhost:${zot_port3} --tls disabled + run regctl registry set localhost:${zot_port1} --tls disabled + run regctl registry set localhost:${zot_port2} --tls disabled + + run regctl artifact put localhost:${zot_port3}/artifact:demo <&3 - return 1 - fi - - if [ ! $(command -v jq) ]; then - echo "you need to install jq as a prerequisite to running the tests" >&3 - return 1 - fi - - return 0 -} - -function setup_file() { - export COSIGN_PASSWORD="" - export COSIGN_OCI_EXPERIMENTAL=1 - export COSIGN_EXPERIMENTAL=1 - - # Verify prerequisites are available - if ! $(verify_prerequisites); then - exit 1 - fi - - # Download test data to folder common for the entire suite, not just this file - skopeo --insecure-policy copy --format=oci docker://ghcr.io/project-zot/golang:1.20 oci:${TEST_DATA_DIR}/golang:1.20 - - - # Setup zot server - local zot_sync_per_root_dir=${BATS_FILE_TMPDIR}/zot-per - local zot_sync_ondemand_root_dir=${BATS_FILE_TMPDIR}/zot-ondemand - - local zot_sync_per_config_file=${BATS_FILE_TMPDIR}/zot_sync_per_config.json - local zot_sync_ondemand_config_file=${BATS_FILE_TMPDIR}/zot_sync_ondemand_config.json - - local zot_minimal_root_dir=${BATS_FILE_TMPDIR}/zot-minimal - local zot_minimal_config_file=${BATS_FILE_TMPDIR}/zot_minimal_config.json - - local oci_data_dir=${BATS_FILE_TMPDIR}/oci - mkdir -p ${zot_sync_per_root_dir} - mkdir -p ${zot_sync_ondemand_root_dir} - mkdir -p ${zot_minimal_root_dir} - mkdir -p ${oci_data_dir} - - cat >${zot_sync_per_config_file} <${zot_sync_ondemand_config_file} <${zot_minimal_config_file} <${trust_policy_file} < config.json -echo "hello world" > artifact.txt -run oras push --plain-http 127.0.0.1:9000/hello-artifact:v2 \ - --config config.json:application/vnd.acme.rocket.config.v1+json artifact.txt:text/plain -d -v - [ "$status" -eq 0 ] - rm -f artifact.txt - rm -f config.json -} - -@test "sync oras artifact periodically" { -# # wait for oras artifact to be copied -run sleep 15s -run oras pull --plain-http 127.0.0.1:8081/hello-artifact:v2 -d -v -[ "$status" -eq 0 ] -grep -q "hello world" artifact.txt -rm -f artifact.txt -} - -@test "sync oras artifact on demand" { -run oras pull --plain-http 127.0.0.1:8082/hello-artifact:v2 -d -v -[ "$status" -eq 0 ] -grep -q "hello world" artifact.txt -rm -f artifact.txt -} - -# sync helm chart -@test "push helm chart" { -run helm package ${BATS_FILE_TMPDIR}/helm-charts/charts/zot -d ${BATS_FILE_TMPDIR} -[ "$status" -eq 0 ] -local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) -run helm push ${BATS_FILE_TMPDIR}/zot-${chart_version}.tgz oci://localhost:9000/zot-chart -[ "$status" -eq 0 ] -} - -@test "sync helm chart periodically" { -# wait for helm chart to be copied -run sleep 15s - -local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) -run helm pull oci://localhost:8081/zot-chart/zot --version ${chart_version} -d ${BATS_FILE_TMPDIR} -[ "$status" -eq 0 ] -} - -@test "sync helm chart on demand" { -local chart_version=$(awk '/version/{printf $2}' ${BATS_FILE_TMPDIR}/helm-charts/charts/zot/Chart.yaml) -run helm pull oci://localhost:8082/zot-chart/zot --version ${chart_version} -d ${BATS_FILE_TMPDIR} -[ "$status" -eq 0 ] -} - -# sync OCI artifacts -@test "push OCI artifact (oci image mediatype) with regclient" { -run regctl registry set localhost:9000 --tls disabled -run regctl registry set localhost:8081 --tls disabled -run regctl registry set localhost:8082 --tls disabled - -run regctl artifact put localhost:9000/artifact:demo <