Manage builds with different combinations of extensions

Files were added to be built whether an extension is on or off.
New build tags were added for each extension, while minimal and extended disappeared.

added custom binary naming depending on extensions used and changed references from binary to binary-extended

added automated blackbox tests for sync, search, scrub, metrics

added contributor guidelines

Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
Alex Stan
2022-04-27 09:00:20 +03:00
committed by Ramkumar Chinchani
parent 616d5f8a6d
commit ada21ed842
67 changed files with 1332 additions and 266 deletions
+31
View File
@@ -0,0 +1,31 @@
## Adding new extensions
As new requirements come and build time extensions need to be added, there are a few things that you have to make sure are present before commiting :
- files that should be included in the binary only with a specific extension must contain the following syntax at the beginning of the file :
//go:build sync will be added automatically by the linter, so only the second line is mandatory .
NOTE: the third line in the example should be blank, otherwise the build tag would be just another comment.
```
//go:build sync
// +build sync
package extensions
...................
```
- when adding a new tag, specify the new order in which multiple tags should be used (bottom of this page)
- for each and every new file that contains functions (functionalities) specific to an extension, one should create a corresponding file that <b>must contain the exact same functions, but no functionalities included</b>. This file must begin with an "anti-tag" (e.g. // +build !sync) which will include this file in binaries that don't include this extension ( in this example, the file won't be used in binaries that include sync extension ). See [extension-sync-disabled.go](extension-sync-disabled.go) for an example.
- when a new extension comes out, the developer should also write some blackbox tests, where a binary that contains the new extension should be tested in a real usage scenario. See [test/blackbox](test/blackbox/sync.bats) folder for multiple extensions examples.
- newly added blackbox tests should have targets in Makefile. You should also add them as Github Workflows, in [.github/workflows/ecosystem-tools.yaml](.github/workflows/ecosystem-tools.yaml)
- with every new extension, you should modify the EXTENSIONS variable in Makefile by adding the new extension. The EXTENSIONS variable represents all extensions and is used in Make targets that require them all (e.g make test).
- the available extensions that can be used at the moment are: <b>sync, scrub, metrics, search, ui_base </b>.
NOTE: When multiple extensions are used, they should be enlisted in the above presented order.
@@ -0,0 +1,25 @@
//go:build !metrics
// +build !metrics
package extensions
import (
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// EnableMetricsExtension ...
func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir string) {
log.Warn().Msg("skipping enabling metrics extension because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
// SetupMetricsRoutes ...
func SetupMetricsRoutes(conf *config.Config, router *mux.Router,
storeController storage.StoreController, log log.Logger,
) {
log.Warn().Msg("skipping setting up metrics routes because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
+39
View File
@@ -0,0 +1,39 @@
//go:build metrics
// +build metrics
package extensions
import (
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir string) {
if config.Extensions.Metrics != nil &&
*config.Extensions.Metrics.Enable &&
config.Extensions.Metrics.Prometheus != nil {
if config.Extensions.Metrics.Prometheus.Path == "" {
config.Extensions.Metrics.Prometheus.Path = "/metrics"
log.Warn().Msg("Prometheus instrumentation Path not set, changing to '/metrics'.")
}
} else {
log.Info().Msg("Metrics config not provided, skipping Metrics config update")
}
}
func SetupMetricsRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
l log.Logger,
) {
// fork a new zerolog child to avoid data race
log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()}
log.Info().Msg("setting up metrics routes")
if config.Extensions.Metrics != nil && *config.Extensions.Metrics.Enable {
router.PathPrefix(config.Extensions.Metrics.Prometheus.Path).
Handler(promhttp.Handler())
}
}
@@ -0,0 +1,19 @@
//go:build !scrub
// +build !scrub
package extensions
import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// EnableScrubExtension ...
func EnableScrubExtension(config *config.Config,
log log.Logger, run bool,
imgStore storage.ImageStore, repo string,
) {
log.Warn().Msg("skipping enabling scrub extension because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
+33
View File
@@ -0,0 +1,33 @@
//go:build scrub
// +build scrub
package extensions
import (
"time"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/extensions/scrub"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// EnableScrubExtension enables scrub extension.
func EnableScrubExtension(config *config.Config, log log.Logger, run bool, imgStore storage.ImageStore, repo string) {
if !run {
if config.Extensions.Scrub != nil &&
config.Extensions.Scrub.Interval != 0 {
minScrubInterval, _ := time.ParseDuration("2h")
if config.Extensions.Scrub.Interval < minScrubInterval {
config.Extensions.Scrub.Interval = minScrubInterval
log.Warn().Msg("Scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.") //nolint:lll // gofumpt conflicts with lll
}
} else {
log.Info().Msg("Scrub config not provided, skipping scrub")
}
} else {
scrub.RunScrubRepo(imgStore, repo, log)
}
}
@@ -0,0 +1,31 @@
//go:build !search && !ui_base
// +build !search,!ui_base
package extensions
import (
"github.com/gorilla/mux"
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// EnableSearchExtension ...
func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) {
log.Warn().Msg("skipping enabling search extension because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
// SetupSearchRoutes ...
func SetupSearchRoutes(conf *config.Config, router *mux.Router,
storeController storage.StoreController, log log.Logger,
) {
log.Warn().Msg("skipping setting up search routes because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
// GetExtensions...
func GetExtensions(config *config.Config) distext.ExtensionList {
return distext.ExtensionList{}
}
@@ -1,45 +1,23 @@
//go:build extended
// +build extended
//go:build search || ui_base
// +build search ui_base
package extensions
import (
"context"
"fmt"
goSync "sync"
"time"
gqlHandler "github.com/99designs/gqlgen/graphql/handler"
"github.com/gorilla/mux"
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
"github.com/prometheus/client_golang/prometheus/promhttp"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/extensions/scrub"
"zotregistry.io/zot/pkg/extensions/search"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
"zotregistry.io/zot/pkg/extensions/sync"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// DownloadTrivyDB ...
func downloadTrivyDB(dbDir string, log log.Logger, updateInterval time.Duration) error {
for {
log.Info().Msg("updating the CVE database")
err := cveinfo.UpdateCVEDb(dbDir, log)
if err != nil {
return err
}
log.Info().Str("DB update completed, next update scheduled after", updateInterval.String()).Msg("")
time.Sleep(updateInterval)
}
}
func EnableExtensions(config *config.Config, log log.Logger, rootDir string) {
func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil {
defaultUpdateInterval, _ := time.ParseDuration("2h")
@@ -59,51 +37,41 @@ func EnableExtensions(config *config.Config, log log.Logger, rootDir string) {
} else {
log.Info().Msg("CVE config not provided, skipping CVE update")
}
}
if config.Extensions.Metrics != nil &&
*config.Extensions.Metrics.Enable &&
config.Extensions.Metrics.Prometheus != nil {
if config.Extensions.Metrics.Prometheus.Path == "" {
config.Extensions.Metrics.Prometheus.Path = constants.DefaultMetricsExtensionRoute
func downloadTrivyDB(dbDir string, log log.Logger, updateInterval time.Duration) error {
for {
log.Info().Msg("updating the CVE database")
log.Warn().Msg(fmt.Sprintf("Prometheus instrumentation Path not set, changing to %s.",
constants.DefaultMetricsExtensionRoute))
err := cveinfo.UpdateCVEDb(dbDir, log)
if err != nil {
return err
}
} else {
log.Info().Msg("Metrics config not provided, skipping Metrics config update")
log.Info().Str("DB update completed, next update scheduled after", updateInterval.String()).Msg("")
time.Sleep(updateInterval)
}
}
// EnableSyncExtension enables sync extension.
func EnableSyncExtension(ctx context.Context, config *config.Config, wg *goSync.WaitGroup,
storeController storage.StoreController, log log.Logger,
func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
l log.Logger,
) {
if config.Extensions.Sync != nil && *config.Extensions.Sync.Enable {
if err := sync.Run(ctx, *config.Extensions.Sync, storeController, wg, log); err != nil {
log.Error().Err(err).Msg("Error encountered while setting up syncing")
}
} else {
log.Info().Msg("Sync registries config not provided or disabled, skipping sync")
}
}
// fork a new zerolog child to avoid data race
log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()}
log.Info().Msg("setting up search routes")
// EnableScrubExtension enables scrub extension.
func EnableScrubExtension(config *config.Config, log log.Logger, run bool, imgStore storage.ImageStore, repo string) {
if !run {
if config.Extensions.Scrub != nil &&
config.Extensions.Scrub.Interval != 0 {
minScrubInterval, _ := time.ParseDuration("2h")
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
var resConfig search.Config
if config.Extensions.Scrub.Interval < minScrubInterval {
config.Extensions.Scrub.Interval = minScrubInterval
log.Warn().Msg("Scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.") //nolint:lll // gofumpt conflicts with lll
}
if config.Extensions.Search.CVE != nil {
resConfig = search.GetResolverConfig(log, storeController, true)
} else {
log.Info().Msg("Scrub config not provided, skipping scrub")
resConfig = search.GetResolverConfig(log, storeController, false)
}
} else {
scrub.RunScrubRepo(imgStore, repo, log)
router.PathPrefix(constants.ExtSearchPrefix).Methods("OPTIONS", "GET", "POST").
Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig)))
}
}
@@ -135,40 +103,3 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
return extensionList
}
// SetupRoutes ...
func SetupRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController, l log.Logger,
) {
// fork a new zerolog child to avoid data race
log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()}
log.Info().Msg("setting up extensions routes")
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
var resConfig search.Config
if config.Extensions.Search.CVE != nil {
resConfig = search.GetResolverConfig(log, storeController, true)
} else {
resConfig = search.GetResolverConfig(log, storeController, false)
}
router.PathPrefix(constants.ExtSearchPrefix).Methods("OPTIONS", "GET", "POST").
Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig)))
}
if config.Extensions.Metrics != nil && *config.Extensions.Metrics.Enable {
router.PathPrefix(config.Extensions.Metrics.Prometheus.Path).
Handler(promhttp.Handler())
}
}
// SyncOneImage syncs one image.
func SyncOneImage(config *config.Config, storeController storage.StoreController,
repoName, reference string, isArtifact bool, log log.Logger,
) error {
log.Info().Msgf("syncing image %s:%s", repoName, reference)
err := sync.OneImage(*config.Extensions.Sync, storeController, repoName, reference, isArtifact, log)
return err
}
+32
View File
@@ -0,0 +1,32 @@
//go:build !sync
// +build !sync
package extensions
import (
"context"
goSync "sync"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// EnableSyncExtension ...
func EnableSyncExtension(ctx context.Context,
config *config.Config, wg *goSync.WaitGroup,
storeController storage.StoreController, log log.Logger,
) {
log.Warn().Msg("skipping enabling sync extension because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
// SyncOneImage ...
func SyncOneImage(config *config.Config, storeController storage.StoreController,
repoName, reference string, isArtifact bool, log log.Logger,
) error {
log.Warn().Msg("skipping syncing on demand because given zot binary doesn't include this feature," +
"please build a binary that does so")
return nil
}
+36
View File
@@ -0,0 +1,36 @@
//go:build sync
// +build sync
package extensions
import (
"context"
goSync "sync"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/extensions/sync"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
func EnableSyncExtension(ctx context.Context, config *config.Config, wg *goSync.WaitGroup,
storeController storage.StoreController, log log.Logger,
) {
if config.Extensions.Sync != nil && *config.Extensions.Sync.Enable {
if err := sync.Run(ctx, *config.Extensions.Sync, storeController, wg, log); err != nil {
log.Error().Err(err).Msg("Error encountered while setting up syncing")
}
} else {
log.Info().Msg("Sync registries config not provided or disabled, skipping sync")
}
}
func SyncOneImage(config *config.Config, storeController storage.StoreController,
repoName, reference string, isArtifact bool, log log.Logger,
) error {
log.Info().Msgf("syncing image %s:%s", repoName, reference)
err := sync.OneImage(*config.Extensions.Sync, storeController, repoName, reference, isArtifact, log)
return err
}
+109
View File
@@ -0,0 +1,109 @@
//go:build sync || metrics
// +build sync metrics
package extensions_test
import (
"context"
"io/ioutil"
"os"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/sync"
"zotregistry.io/zot/pkg/test"
)
func TestEnableExtension(t *testing.T) {
Convey("Verify log if sync disabled in config", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
falseValue := false
syncConfig := &sync.Config{
Enable: &falseValue,
Registries: []sync.RegistryConfig{},
}
// conf.Extensions.Sync.Enable = &falseValue
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Sync = syncConfig
conf.HTTP.Port = port
logFile, err := ioutil.TempFile(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
conf.Log.Level = "info"
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
defer func() {
ctx := context.Background()
_ = ctlr.Server.Shutdown(ctx)
}()
ctlr.Config.Storage.RootDirectory = globalDir
go func() {
if err := ctlr.Run(context.Background()); err != nil {
return
}
}()
test.WaitTillServerReady(baseURL)
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"Sync registries config not provided or disabled, skipping sync")
})
}
func TestMetricsExtension(t *testing.T) {
Convey("Verify Metrics enabled for storage subpaths", t, func() {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
baseURL := test.GetBaseURL(port)
logFile, err := ioutil.TempFile(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defaultValue := true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Metrics = &extconf.MetricsConfig{
Enable: &defaultValue,
Prometheus: &extconf.PrometheusConfig{},
}
conf.Log.Level = "info"
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{}
ctlr.Config.Storage.RootDirectory = globalDir
ctlr.Config.Storage.SubPaths = subPaths
go func() {
if err := ctlr.Run(context.Background()); err != nil {
return
}
}()
test.WaitTillServerReady(baseURL)
data, _ := os.ReadFile(logFile.Name())
So(string(data), ShouldContainSubstring,
"Prometheus instrumentation Path not set, changing to '/metrics'.")
})
}
-63
View File
@@ -1,63 +0,0 @@
//go:build minimal
// +build minimal
package extensions
import (
"context"
goSync "sync"
"time"
"github.com/gorilla/mux"
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// nolint: deadcode,unused
func downloadTrivyDB(dbDir string, log log.Logger, updateInterval time.Duration) error {
return nil
}
// EnableExtensions ...
func EnableExtensions(config *config.Config, log log.Logger, rootDir string) {
log.Warn().Msg("skipping enabling extensions because given zot binary doesn't support " +
"any extensions, please build zot full binary for this feature")
}
// GetExtensions...
func GetExtensions(config *config.Config) distext.ExtensionList {
return distext.ExtensionList{}
}
// EnableSyncExtension ...
func EnableSyncExtension(ctx context.Context, config *config.Config, wg *goSync.WaitGroup,
storeController storage.StoreController, log log.Logger,
) {
log.Warn().Msg("skipping enabling sync extension because given zot binary doesn't support any extensions," +
"please build zot full binary for this feature")
}
// EnableScrubExtension ...
func EnableScrubExtension(config *config.Config, log log.Logger, run bool, imgStore storage.ImageStore, repo string) {
log.Warn().Msg("skipping enabling scrub extension because given zot binary doesn't support any extensions," +
"please build zot full binary for this feature")
}
// SetupRoutes ...
func SetupRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController, log log.Logger,
) {
log.Warn().Msg("skipping setting up extensions routes because given zot binary doesn't support " +
"any extensions, please build zot full binary for this feature")
}
// SyncOneImage ...
func SyncOneImage(config *config.Config, storeController storage.StoreController,
repoName, reference string, isArtifact bool, log log.Logger,
) error {
log.Warn().Msg("skipping syncing on demand because given zot binary doesn't support any extensions," +
"please build zot full binary for this feature")
return nil
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build metrics
// +build metrics
package monitoring
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build minimal
// +build minimal
//go:build !metrics
// +build !metrics
// nolint: varnamelen,forcetypeassert
package monitoring
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build minimal
// +build minimal
//go:build !metrics
// +build !metrics
package monitoring
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build metrics
// +build metrics
package monitoring_test
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build scrub
// +build scrub
package scrub
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build scrub
// +build scrub
package scrub_test
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build search
// +build search
package common_test
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build search
// +build search
// nolint:lll,gosimple
package cveinfo_test
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build search
// +build search
// nolint: gochecknoinits
package digestinfo_test
+71
View File
@@ -0,0 +1,71 @@
//go:build !sync
// +build !sync
package sync_test
import (
"context"
"io/ioutil"
"os"
"testing"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/sync"
"zotregistry.io/zot/pkg/test"
)
func TestSyncExtension(t *testing.T) {
Convey("Make a new controller", t, func() {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
globalDir := t.TempDir()
defaultValue := true
logFile, err := ioutil.TempFile(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name())
conf.HTTP.Port = port
conf.Storage.RootDirectory = globalDir
conf.Storage.Commit = true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Sync = &sync.Config{
Enable: &defaultValue,
}
conf.Log.Level = "warn"
conf.Log.Output = logFile.Name()
ctlr := api.NewController(conf)
go func() {
if err := ctlr.Run(context.Background()); err != nil {
return
}
}()
defer func() {
_ = ctlr.Server.Shutdown(context.Background())
}()
test.WaitTillServerReady(baseURL)
Convey("verify sync is skipped when binary doesn't include it", func() {
resp, err := resty.R().
Head(baseURL + "/v2/" + "invalid" + "/manifests/invalid:0.0.2")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"skipping syncing on demand because given zot binary doesn't include "+
"this feature,please build a binary that does so")
})
})
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build extended
// +build extended
//go:build sync
// +build sync
package sync_test