Files
zot/pkg/cli/server/extensions_test.go
T
Ramkumar Chinchani 4e4d00a0a6 feat: add trivy-based sbom artifact generation support (#4088)
fixes issue #4067

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
2026-05-23 23:24:12 -07:00

1887 lines
43 KiB
Go

//go:build sync && scrub && metrics && search && userprefs && mgmt && imagetrust && events
package server_test
import (
"fmt"
"net/http"
"os"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/api/config"
cli "zotregistry.dev/zot/v2/pkg/cli/server"
. "zotregistry.dev/zot/v2/pkg/test/common"
)
const readLogFileTimeout = 5 * time.Second
func TestVerifyExtensionsConfig(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("Test verify CVE warn for remote storage", t, func(c C) {
content := fmt.Sprintf(`{
"storage":{
"rootDirectory":"%s",
"dedupe":true,
"remoteCache":false,
"storageDriver":{
"name":"s3",
"rootdirectory":"/zot",
"region":"us-east-2",
"bucket":"zot-storage",
"secure":true,
"skipverify":false
}
},
"http":{
"address":"127.0.0.1",
"port":"8080"
},
"extensions":{
"search": {
"enable": true,
"cve": {
"updateInterval": "24h"
}
}
}
}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
content = fmt.Sprintf(`{
"storage":{
"rootDirectory":"%s",
"dedupe":true,
"remoteCache":false,
"subPaths":{
"/a": {
"rootDirectory": "%s",
"dedupe": false,
"storageDriver":{
"name":"s3",
"rootdirectory":"/zot-a",
"region":"us-east-2",
"bucket":"zot-storage",
"secure":true,
"skipverify":false
}
}
}
},
"http":{
"address":"127.0.0.1",
"port":"8080"
},
"extensions":{
"search": {
"enable": true,
"cve": {
"updateInterval": "24h"
}
}
}
}`, t.TempDir(), t.TempDir())
tmpfile = MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
})
Convey("Test verify w/ sync and w/o filesystem storage", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s", "storageDriver": {"name": "s3"}},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 1, "retryDelay": "10s"}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
})
Convey("Test verify w/ sync and w/ filesystem storage", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 1, "retryDelay": "10s"}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldBeNil)
})
Convey("Test verify with bad sync prefixes", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 1, "retryDelay": "10s",
"content": [{"prefix":"[repo^&["}]}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
})
Convey("Test verify with bad sync content config", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 1, "retryDelay": "10s",
"content": [{"prefix":"zot-repo","stripPrefix":true,"destination":"/"}]}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
})
Convey("Test verify with good sync content config", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 1, "retryDelay": "10s",
"content": [{"prefix":"zot-repo/*","stripPrefix":true,"destination":"/"}]}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
err := cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
})
Convey("Test verify sync config default tls value", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 1, "retryDelay": "10s",
"content": [{"prefix":"repo**"}]}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
err := cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
})
Convey("Test verify sync without retry options", t, func(c C) {
content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"maxRetries": 10, "content": [{"prefix":"repo**"}]}]}}}`, t.TempDir())
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "verify", tmpfile}
So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
})
}
func TestValidateExtensionsConfig(t *testing.T) {
Convey("Legacy extensions should not error", t, func(c C) {
config := config.New()
content := `{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"mgmt": {
"enable": "true"
},
"apikey": {
"enable": "true"
}
}
}`
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(config, tmpfile)
So(err, ShouldBeNil)
})
Convey("Test missing extensions for UI to work", t, func(c C) {
config := config.New()
content := `{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"ui": {
"enable": "true"
}
}
}`
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(config, tmpfile)
So(err, ShouldNotBeNil)
})
Convey("Test enabling UI extension with all prerequisites", t, func(c C) {
config := config.New()
content := `{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"ui": {
"enable": "true"
},
"search": {
"enable": "true"
}
}
}`
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(config, tmpfile)
So(err, ShouldBeNil)
})
Convey("Test extension are implicitly enabled", t, func(c C) {
config := config.New()
content := `{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"ui": {},
"search": {},
"metrics": {},
"trust": {},
"scrub": {}
}
}`
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(config, tmpfile)
So(err, ShouldBeNil)
So(config.Extensions.UI, ShouldNotBeNil)
So(*config.Extensions.UI.Enable, ShouldBeTrue)
So(config.Extensions.Search, ShouldNotBeNil)
So(*config.Extensions.Search.Enable, ShouldBeTrue)
So(config.Extensions.Trust, ShouldNotBeNil)
So(*config.Extensions.Trust.Enable, ShouldBeTrue)
So(*config.Extensions.Metrics, ShouldNotBeNil)
So(*config.Extensions.Metrics.Enable, ShouldBeTrue)
So(config.Extensions.Scrub, ShouldNotBeNil)
So(*config.Extensions.Scrub.Enable, ShouldBeTrue)
})
}
func TestServeExtensions(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("config file with no extensions", t, func(c C) {
port := GetFreePort()
baseURL := GetBaseURL(port)
logPath := MakeTempFilePath(t, "zot-log.txt")
tmpFile := t.TempDir()
content := fmt.Sprintf(`{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
}
}`, tmpFile, port, logPath)
cfgfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "serve", cfgfile}
go func() {
Convey("run", t, func() {
err := cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
})
}()
WaitTillServerReady(baseURL)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "\"Extensions\":null")
})
Convey("config file with empty extensions", t, func(c C) {
port := GetFreePort()
baseURL := GetBaseURL(port)
logPath := MakeTempFilePath(t, "zot-log.txt")
tmpFile := t.TempDir()
content := fmt.Sprintf(`{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
}
}`, tmpFile, port, logPath)
cfgfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "serve", cfgfile}
go func() {
Convey("run", t, func() {
err := cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
})
}()
WaitTillServerReady(baseURL)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"UI\":null,\"Mgmt\":null") //nolint:lll // gofumpt conflicts with lll
})
}
func testWithMetricsEnabled(t *testing.T, rootDir string, cfgContentFormat string) {
t.Helper()
port := GetFreePort()
baseURL := GetBaseURL(port)
logPath := MakeTempFilePath(t, "zot-log.txt")
content := fmt.Sprintf(cfgContentFormat, rootDir, port, logPath)
cfgfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "serve", cfgfile}
go func() {
Convey("run", t, func() {
err := cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
})
}()
WaitTillServerReady(baseURL)
resp, err := resty.R().Get(baseURL + "/metrics")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
respStr := string(resp.Body())
So(respStr, ShouldContainSubstring, "zot_info")
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Metrics\":{\"Enable\":true,\"Prometheus\":{\"Path\":\"/metrics\"}}")
}
func TestServeMetricsExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("no explicit enable", t, func(c C) {
tmpFile := t.TempDir()
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"metrics": {
}
}
}`
testWithMetricsEnabled(t, tmpFile, content)
})
Convey("no explicit enable but with prometheus parameter", t, func(c C) {
tmpFile := t.TempDir()
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"metrics": {
"prometheus": {
"path": "/metrics"
}
}
}
}`
testWithMetricsEnabled(t, tmpFile, content)
})
Convey("with explicit enable, but without prometheus parameter", t, func(c C) {
tmpFile := t.TempDir()
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"metrics": {
"enable": true
}
}
}`
testWithMetricsEnabled(t, tmpFile, content)
})
Convey("with explicit disable", t, func(c C) {
port := GetFreePort()
baseURL := GetBaseURL(port)
logPath := MakeTempFilePath(t, "zot-log.txt")
tmpFile := t.TempDir()
content := fmt.Sprintf(`{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"metrics": {
"enable": false
}
}
}`, tmpFile, port, logPath)
cfgfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "serve", cfgfile}
go func() {
Convey("run", t, func() {
err := cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
})
}()
WaitTillServerReady(baseURL)
resp, err := resty.R().Get(baseURL + "/metrics")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Metrics\":{\"Enable\":false,\"Prometheus\":{\"Path\":\"/metrics\"}}") //nolint:lll // gofumpt conflicts with lll
})
}
func TestServeSyncExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("sync implicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"registries": [{
"urls": ["http://localhost:8080"],
"tlsVerify": false,
"onDemand": true,
"maxRetries": 3,
"retryDelay": "15m",
"certDir": "",
"content":[
{
"prefix": "zot-test",
"tags": {
"regex": ".*",
"semver": true
}
}
]
}]
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":{\"Enable\":true")
})
Convey("sync explicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"enable": true,
"registries": [{
"urls": ["http://localhost:8080"],
"tlsVerify": false,
"onDemand": true,
"maxRetries": 3,
"retryDelay": "15m",
"certDir": "",
"content":[
{
"prefix": "zot-test",
"tags": {
"regex": ".*",
"semver": true
}
}
]
}]
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":{\"Enable\":true")
})
Convey("sync explicitly disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"enable": false,
"registries": [{
"urls": ["http://127.0.0.1:8080"],
"tlsVerify": false,
"certDir": "",
"maxRetries": 3,
"retryDelay": "15m"
}]
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":{\"Enable\":false")
})
}
func TestServeScrubExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("scrub implicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"scrub": {
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
dataStr := string(data)
So(dataStr, ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":{\"Enable\":true,\"Interval\":86400000000000},\"Lint\":null") //nolint:lll // gofumpt conflicts with lll
So(dataStr, ShouldNotContainSubstring,
"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
})
Convey("scrub implicitly enabled, but with scrub interval param set", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"scrub": {
"interval": "1h"
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
// Even if in config we specified scrub interval=1h, the minimum interval is 2h
dataStr := string(data)
So(dataStr, ShouldContainSubstring, "\"Scrub\":{\"Enable\":true,\"Interval\":7200000000000}")
So(dataStr, ShouldContainSubstring,
"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
})
Convey("scrub explicitly enabled, but without scrub interval param set", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"scrub": {
"enable": true
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
dataStr := string(data)
So(dataStr, ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":{\"Enable\":true,\"Interval\":86400000000000},\"Lint\":null") //nolint:lll // gofumpt conflicts with lll
So(dataStr, ShouldNotContainSubstring,
"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
})
Convey("scrub explicitly disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"scrub": {
"enable": false
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
dataStr := string(data)
So(dataStr, ShouldContainSubstring, "\"Scrub\":{\"Enable\":false,\"Interval\":86400000000000}")
So(dataStr, ShouldContainSubstring, "scrub config not provided, skipping scrub")
So(dataStr, ShouldNotContainSubstring,
"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
})
}
func TestServeLintExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("lint enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"lint": {
"enable": "true",
"mandatoryAnnotations": ["annot1"]
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enable\":true,\"MandatoryAnnotations\":") //nolint:lll // gofumpt conflicts with lll
})
Convey("lint enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"lint": {
"enable": "false"
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enable\":false,\"MandatoryAnnotations\":null}") //nolint:lll // gofumpt conflicts with lll
})
}
func TestServeSearchEnabled(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("search implicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"search": {
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
// to avoid data race when multiple go routines write to trivy DB instance.
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}`
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
})
}
func TestServeSearchEnabledDefaultCVEDB(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("search implicitly enabled with CVE param set", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"search": {
"cve": {
"updateInterval": "1h"
}
}
}
}`
logPath, rootDir, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
// to avoid data race when multiple go routines write to trivy DB instance.
WaitTillTrivyDBDownloadStarted(rootDir)
// The default config handling logic will convert the 1h interval to a 2h interval
substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"," +
"\"VulnSeveritySources\":[\"auto\"],\"SBOM\":null}}}"
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = ReadLogFileAndSearchString(logPath, "updating cve-db", readLogFileTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
})
Convey("VulnSeveritySources defaults to [auto] when trivy block is empty", t, func(c C) {
cfg := config.New()
// mapstructure omits an all-empty CVE; at least updateInterval (or any non-zero cve/trivy
// field) must be present for CVE to unmarshal non-nil before defaults run.
content := `{
"storage": { "rootDirectory": "/tmp/zot" },
"http": { "address": "127.0.0.1", "port": "8080" },
"extensions": {
"search": {
"enable": true,
"cve": { "updateInterval": "24h", "trivy": { } }
}
}
}`
configPath := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, configPath)
So(err, ShouldBeNil)
So(cfg.Extensions, ShouldNotBeNil)
So(cfg.Extensions.Search, ShouldNotBeNil)
So(cfg.Extensions.Search.CVE, ShouldNotBeNil)
So(cfg.Extensions.Search.CVE.Trivy, ShouldNotBeNil)
So(cfg.Extensions.Search.CVE.Trivy.VulnSeveritySources, ShouldResemble, []string{"auto"})
})
Convey("VulnSeveritySources respects explicit list", t, func(c C) {
cfg := config.New()
content := `{
"storage": { "rootDirectory": "/tmp/zot" },
"http": { "address": "127.0.0.1", "port": "8080" },
"extensions": {
"search": {
"enable": true,
"cve": { "trivy": { "vulnSeveritySources": ["nvd", "ghsa"] } }
}
}
}`
configPath := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, configPath)
So(err, ShouldBeNil)
So(cfg.Extensions.Search.CVE.Trivy.VulnSeveritySources, ShouldResemble, []string{"nvd", "ghsa"})
})
Convey("CVE with only updateInterval (no trivy key) gets VulnSeveritySources [auto]", t, func(c C) {
cfg := config.New()
content := `{
"storage": { "rootDirectory": "/tmp/zot" },
"http": { "address": "127.0.0.1", "port": "8080" },
"extensions": {
"search": {
"enable": true,
"cve": { "updateInterval": "24h" }
}
}
}`
configPath := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, configPath)
So(err, ShouldBeNil)
So(cfg.Extensions.Search.CVE.Trivy, ShouldNotBeNil)
So(cfg.Extensions.Search.CVE.Trivy.VulnSeveritySources, ShouldResemble, []string{"auto"})
})
}
func TestServeSearchEnabledNoCVE(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("search explicitly enabled, but CVE parameter not set", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"search": {
"enable": true
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}` //nolint:lll // gofumpt conflicts with lll
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
})
}
func TestServeSearchDisabled(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("search explicitly disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"search": {
"enable": false,
"cve": {
"updateInterval": "3h"
}
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
dataStr := string(data)
So(dataStr, ShouldContainSubstring,
`"Search":{"Enable":false,"CVE":{"UpdateInterval":10800000000000,"Trivy":null}`)
So(dataStr, ShouldContainSubstring, "cve config not provided, skipping cve-db update")
So(dataStr, ShouldNotContainSubstring,
"CVE update interval set to too-short interval < 2h, changing update duration to 2 hours and continuing.")
})
}
func TestServeMgmtExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("Mgmt implicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"search": {
"enable": true
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath, "setting up mgmt routes", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Mgmt disabled - Search unconfigured", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Mgmt disabled - extensions missing", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
}
func TestServeImageTrustExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("Trust explicitly disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"trust": {
"enable": false
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Trust explicitly enabled - but cosign and notation disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"trust": {
"enable": true
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Trust explicitly enabled - cosign and notation enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"trust": {
"enable": true,
"cosign": true,
"notation": true
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath,
"setting up image trust routes", 10*time.Second)
defer func() {
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = ReadLogFileAndSearchString(logPath,
"setting up notation route", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = ReadLogFileAndSearchString(logPath,
"setting up cosign route", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
}
func TestOverlappingSyncRetentionConfig(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("Test verify without overlapping sync and retention", t, func(c C) {
content := `{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "%s",
"gc": true,
"gcDelay": "2h",
"gcInterval": "1h",
"retention": {
"policies": [
{
"repositories": ["infra/*", "prod/*"],
"deleteReferrers": false,
"keepTags": [{
"patterns": ["v4.*", ".*-prod"]
},
{
"patterns": ["v3.*", ".*-prod"],
"pulledWithin": "168h"
}]
}
]
}
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": [
"https://registry1:5000"
],
"content": [
{
"prefix": "infra/*",
"tags": {
"regex": "v4.*",
"semver": true
}
}
]
}
]
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldNotContainSubstring, "overlapping sync content")
})
Convey("Test verify with overlapping sync and retention - retention would remove v4 tags", t, func(c C) {
content := `{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "%s",
"gc": true,
"gcDelay": "2h",
"gcInterval": "1h",
"retention": {
"policies": [
{
"repositories": ["infra/*", "prod/*"],
"keepTags": [{
"patterns": ["v2.*", ".*-prod"]
},
{
"patterns": ["v3.*", ".*-prod"]
}]
}
]
}
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": [
"https://registry1:5000"
],
"content": [
{
"prefix": "infra/*",
"tags": {
"regex": "4.*",
"semver": true
}
}
]
}
]
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "overlapping sync content\":{\"Prefix\":\"infra/*")
})
Convey("Test verify with overlapping sync and retention - retention would remove tags from repo", t, func(c C) {
content := `{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "%s",
"gc": true,
"gcDelay": "2h",
"gcInterval": "1h",
"retention": {
"dryRun": false,
"delay": "24h",
"policies": [
{
"repositories": ["tmp/**"],
"keepTags": [{
"patterns": ["v1.*"]
}]
}
]
}
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": [
"https://registry1:5000"
],
"content": [
{
"prefix": "**",
"destination": "/tmp",
"stripPrefix": true
}
]
}
]
}
}
}
`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "overlapping sync content\":{\"Prefix\":\"**")
})
Convey("Test verify with overlapping sync and retention - retention would remove tags from subpath", t, func(c C) {
content := `{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "%s",
"gc": true,
"gcDelay": "2h",
"gcInterval": "1h",
"subPaths": {
"/synced": {
"rootDirectory": "/tmp/zot2",
"dedupe": true,
"retention": {
"policies": [
{
"repositories": ["infra/*", "prod/*"],
"deleteReferrers": false,
"keepTags": [{
}]
}
]
}
}
}
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": [
"https://registry1:5000"
],
"content": [
{
"prefix": "prod/*",
"destination": "/synced"
}
]
}
]
}
}
}
`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
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) {
content := `{
"distSpecVersion": "1.1.1",
"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, content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
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()
logPath := MakeTempFilePath(t, "zot-log.txt")
content := fmt.Sprintf(`{
"distSpecVersion": "1.1.1",
"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, logPath)
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "serve", tmpfile}
err := cli.NewServerRootCmd().Execute()
So(err, ShouldNotBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
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()
logPath := MakeTempFilePath(t, "zot-log.txt")
content := fmt.Sprintf(`{
"distSpecVersion": "1.1.1",
"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, logPath)
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
os.Args = []string{"cli_test", "serve", tmpfile}
err := cli.NewServerRootCmd().Execute()
So(err, ShouldNotBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified")
})
}
func TestEventsExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("Events explicitly disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"events": {
"enable": false
}
}
}`
logPath, _, err := runCLIWithConfig(t, content)
So(err, ShouldBeNil)
found, err := ReadLogFileAndSearchString(logPath,
"events disabled in configuration", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Unsupported event sink", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"events": {
"enable": true,
"sinks": [{
"type": "unsupported"
}]
}
}
}`
_, _, err := runCLIWithConfig(t, content)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, zerr.ErrUnsupportedEventSink.Error())
})
}