mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
refactor(extensions)!: refactor the extensions URLs and errors (#1636)
BREAKING CHANGE: The functionality provided by the mgmt endpoint has beed redesigned - see details below BREAKING CHANGE: The API keys endpoint has been moved - see details below BREAKING CHANGE: The mgmt extension config has been removed - endpoint is now enabled by having both the search and the ui extensions enabled BREAKING CHANGE: The API keys configuration has been moved from extensions to http>auth>apikey mgmt and imagetrust extensions: - separate the _zot/ext/mgmt into 3 separate endpoints: _zot/ext/auth, _zot/ext/notation, _zot/ext/cosign - signature verification logic is in a separate `imagetrust` extension - better hanling or errors in case of signature uploads: logging and error codes (more 400 and less 500 errors) - add authz on signature uploads (and add a new middleware in common for this purpose) - remove the mgmt extension configuration - it is now enabled if the UI and the search extensions are enabled userprefs estension: - userprefs are enabled if both search and ui extensions are enabled (as opposed to just search) apikey extension is removed and logic moved into the api folder - Move apikeys code out of pkg/extensions and into pkg/api - Remove apikey configuration options from the extensions configuration and move it inside the http auth section - remove the build label apikeys other changes: - move most of the logic adding handlers to the extensions endpoints out of routes.go and into the extensions files. - add warnings in case the users are still using configurations with the obsolete settings for mgmt and api keys - add a new function in the extension package which could be a single point of starting backgroud tasks for all extensions - more clear methods for verifying specific extensions are enabled - fix http methods paired with the UI handlers - rebuild swagger docs Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
+143
-90
@@ -1,14 +1,12 @@
|
||||
//go:build sync && scrub && metrics && search && apikey
|
||||
// +build sync,scrub,metrics,search,apikey
|
||||
//go:build sync && scrub && metrics && search && userprefs && mgmt && imagetrust
|
||||
// +build sync,scrub,metrics,search,userprefs,mgmt,imagetrust
|
||||
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -640,7 +638,7 @@ func TestServeSearchEnabled(t *testing.T) {
|
||||
|
||||
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}`
|
||||
|
||||
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
@@ -691,7 +689,7 @@ func TestServeSearchEnabledCVE(t *testing.T) {
|
||||
substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
|
||||
"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"}}}"
|
||||
|
||||
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
|
||||
defer func() {
|
||||
if !found {
|
||||
@@ -704,7 +702,7 @@ func TestServeSearchEnabledCVE(t *testing.T) {
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
found, err = readLogFileAndSearchString(logPath, "updating the CVE database", readLogFileTimeout)
|
||||
found, err = ReadLogFileAndSearchString(logPath, "updating the CVE database", readLogFileTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
@@ -741,7 +739,7 @@ func TestServeSearchEnabledNoCVE(t *testing.T) {
|
||||
defer os.Remove(logPath) // clean up
|
||||
|
||||
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}` //nolint:lll // gofumpt conflicts with lll
|
||||
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
@@ -815,20 +813,31 @@ func TestServeMgmtExtension(t *testing.T) {
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"Mgmt": {
|
||||
"ui": {
|
||||
"enable": true
|
||||
},
|
||||
"search": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
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), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":true}")
|
||||
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", t, func(c C) {
|
||||
Convey("Mgmt disabled - UI unconfigured", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
@@ -842,27 +851,66 @@ func TestServeMgmtExtension(t *testing.T) {
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"Mgmt": {
|
||||
"enable": "false"
|
||||
"search": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
defer os.Remove(logPath) // clean up
|
||||
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.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":false}")
|
||||
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 TestServeAPIKeyExtension(t *testing.T) {
|
||||
func TestServeImageTrustExtension(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("apikey implicitly enabled", t, func(c C) {
|
||||
Convey("Trust explicitly disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
@@ -876,20 +924,29 @@ func TestServeAPIKeyExtension(t *testing.T) {
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"trust": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
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), ShouldContainSubstring, "\"APIKey\":{\"Enable\":true}")
|
||||
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("apikey disabled", t, func(c C) {
|
||||
Convey("Trust explicitly enabled - but cosign and notation disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
@@ -903,79 +960,75 @@ func TestServeAPIKeyExtension(t *testing.T) {
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": "false"
|
||||
"trust": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
defer os.Remove(logPath) // clean up
|
||||
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.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":false}")
|
||||
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 readLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { //nolint:unparam,lll
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancelFunc()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
default:
|
||||
content, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(string(content), stringToMatch) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run cli and return output.
|
||||
func runCLIWithConfig(tempDir string, config string) (string, error) {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
|
||||
logFile, err := os.CreateTemp(tempDir, "zot-log*.txt")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cfgfile, err := os.CreateTemp(tempDir, "zot-test*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config = fmt.Sprintf(config, tempDir, port, logFile.Name())
|
||||
|
||||
_, err = cfgfile.Write([]byte(config))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cfgfile.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
os.Args = []string{"cli_test", "serve", cfgfile.Name()}
|
||||
|
||||
go func() {
|
||||
err = cli.NewServerRootCmd().Execute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
WaitTillServerReady(baseURL)
|
||||
|
||||
return logFile.Name(), nil
|
||||
}
|
||||
|
||||
+25
-18
@@ -757,10 +757,10 @@ func TestOutputFormat(t *testing.T) {
|
||||
`"variant":""},"isSigned":false,"downloadCount":0,`+
|
||||
`"layers":[{"size":"","digest":"sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6",`+
|
||||
`"score":0}],"history":null,"vulnerabilities":{"maxSeverity":"","count":0},`+
|
||||
`"referrers":null,"artifactType":""}],"size":"123445",`+
|
||||
`"referrers":null,"artifactType":"","signatureInfo":null}],"size":"123445",`+
|
||||
`"downloadCount":0,"lastUpdated":"0001-01-01T00:00:00Z","description":"","isSigned":false,"licenses":"",`+
|
||||
`"labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",`+
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}`+"\n")
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}`+"\n")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
@@ -788,10 +788,10 @@ func TestOutputFormat(t *testing.T) {
|
||||
`issigned: false downloadcount: 0 layers: - size: "" `+
|
||||
`digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+
|
||||
`size: "123445" downloadcount: 0 `+
|
||||
`signatureinfo: [] size: "123445" downloadcount: 0 `+
|
||||
`lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+
|
||||
`title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: "" `+
|
||||
`count: 0 referrers: []`,
|
||||
`count: 0 referrers: [] signatureinfo: []`,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -822,10 +822,10 @@ func TestOutputFormat(t *testing.T) {
|
||||
`issigned: false downloadcount: 0 layers: - size: "" `+
|
||||
`digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+
|
||||
`size: "123445" downloadcount: 0 `+
|
||||
`signatureinfo: [] size: "123445" downloadcount: 0 `+
|
||||
`lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+
|
||||
`title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: `+
|
||||
`"" count: 0 referrers: []`,
|
||||
`"" count: 0 referrers: [] signatureinfo: []`,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
@@ -886,10 +886,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
||||
`"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` +
|
||||
`"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` +
|
||||
`"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"artifactType":""}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` +
|
||||
`"referrers":null,"artifactType":"","signatureInfo":null}],` +
|
||||
`"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` +
|
||||
`"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` +
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}` + "\n" +
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n" +
|
||||
`{"repoName":"repo7","tag":"test:2.0",` +
|
||||
`"digest":"sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06",` +
|
||||
`"mediaType":"application/vnd.oci.image.manifest.v1+json",` +
|
||||
@@ -898,10 +899,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
||||
`"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` +
|
||||
`"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` +
|
||||
`"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"artifactType":""}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` +
|
||||
`"referrers":null,"artifactType":"","signatureInfo":null}],` +
|
||||
`"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` +
|
||||
`"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` +
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}` + "\n"
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n"
|
||||
// Output is supposed to be in json lines format, keep all spaces as is for verification
|
||||
So(buff.String(), ShouldEqual, expectedStr)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -928,10 +930,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
||||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: [] ` +
|
||||
`--- reponame: repo7 tag: test:2.0 ` +
|
||||
`digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` +
|
||||
`mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` +
|
||||
@@ -940,10 +943,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
||||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: []`
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []`
|
||||
So(strings.TrimSpace(str), ShouldEqual, expectedStr)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
@@ -969,10 +973,12 @@ func TestOutputFormatGQL(t *testing.T) {
|
||||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] signatureinfo: [] ` +
|
||||
`--- reponame: repo7 tag: test:2.0 ` +
|
||||
`digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` +
|
||||
`mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` +
|
||||
@@ -981,10 +987,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
||||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: []`
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []`
|
||||
So(strings.TrimSpace(str), ShouldEqual, expectedStr)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
+37
-26
@@ -313,13 +313,18 @@ func validateCacheConfig(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
func validateExtensionsConfig(cfg *config.Config) error {
|
||||
if cfg.Extensions != nil && cfg.Extensions.Mgmt != nil {
|
||||
log.Warn().Msg("The mgmt extensions configuration option has been made redundant and will be ignored.")
|
||||
}
|
||||
|
||||
if cfg.Extensions != nil && cfg.Extensions.APIKey != nil {
|
||||
log.Warn().Msg("The apikey extension configuration will be ignored as API keys " +
|
||||
"are now configurable in the HTTP settings.")
|
||||
}
|
||||
|
||||
if cfg.Extensions != nil && cfg.Extensions.UI != nil && cfg.Extensions.UI.Enable != nil && *cfg.Extensions.UI.Enable {
|
||||
if cfg.Extensions.Mgmt == nil || !*cfg.Extensions.Mgmt.Enable {
|
||||
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without mgmt extension.")
|
||||
|
||||
return errors.ErrBadConfig
|
||||
}
|
||||
|
||||
// it would make sense to also check for mgmt and user prefs to be enabled,
|
||||
// but those are both enabled by having the search and ui extensions enabled
|
||||
if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
|
||||
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
|
||||
|
||||
@@ -513,18 +518,18 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
||||
config.Extensions.Scrub = &extconf.ScrubConfig{}
|
||||
}
|
||||
|
||||
_, ok = extMap["mgmt"]
|
||||
_, ok = extMap["trust"]
|
||||
if ok {
|
||||
// we found a config like `"extensions": {"mgmt:": {}}`
|
||||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Mgmt = &extconf.MgmtConfig{}
|
||||
// we found a config like `"extensions": {"trust:": {}}`
|
||||
// Note: In case trust is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Trust = &extconf.ImageTrustConfig{}
|
||||
}
|
||||
|
||||
_, ok = extMap["apikey"]
|
||||
_, ok = extMap["ui"]
|
||||
if ok {
|
||||
// we found a config like `"extensions": {"mgmt:": {}}`
|
||||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.APIKey = &extconf.APIKeyConfig{}
|
||||
// we found a config like `"extensions": {"ui:": {}}`
|
||||
// Note: In case UI is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.UI = &extconf.UIConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,18 +591,6 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Mgmt != nil {
|
||||
if config.Extensions.Mgmt.Enable == nil {
|
||||
config.Extensions.Mgmt.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.APIKey != nil {
|
||||
if config.Extensions.APIKey.Enable == nil {
|
||||
config.Extensions.APIKey.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Scrub != nil {
|
||||
if config.Extensions.Scrub.Enable == nil {
|
||||
config.Extensions.Scrub.Enable = &defaultVal
|
||||
@@ -607,6 +600,18 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
||||
config.Extensions.Scrub.Interval = 24 * time.Hour //nolint: gomnd
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.UI != nil {
|
||||
if config.Extensions.UI.Enable == nil {
|
||||
config.Extensions.UI.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Trust != nil {
|
||||
if config.Extensions.Trust.Enable == nil {
|
||||
config.Extensions.Trust.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !config.Storage.GC && viperInstance.Get("storage::gcdelay") == nil {
|
||||
@@ -663,6 +668,12 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
||||
config.Storage.SubPaths[name] = storageConfig
|
||||
}
|
||||
}
|
||||
|
||||
// if OpenID authentication is enabled,
|
||||
// API Keys are also enabled in order to provide data path authentication
|
||||
if config.HTTP.Auth != nil && config.HTTP.Auth.OpenID != nil {
|
||||
config.HTTP.Auth.APIKey = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateDistSpecVersion(config *config.Config) {
|
||||
|
||||
+233
-35
@@ -1083,7 +1083,7 @@ func TestVerify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateExtensionsConfig(t *testing.T) {
|
||||
Convey("Test missing extensions for UI to work", t, func(c C) {
|
||||
Convey("Legacy extensions should not error", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
@@ -1100,40 +1100,39 @@ func TestValidateExtensionsConfig(t *testing.T) {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {
|
||||
"enable": "true"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test missing extensions for UI to work", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{
|
||||
"storage": {
|
||||
"rootDirectory": "%/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {
|
||||
"enable": "true"
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": "true"
|
||||
},
|
||||
"apikey": {
|
||||
"enable": "true"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test missing extensions for UI to work", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
content := []byte(`{
|
||||
"storage": {
|
||||
"rootDirectory": "%/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {
|
||||
"enable": "true"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
@@ -1143,7 +1142,7 @@ func TestValidateExtensionsConfig(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test missing mgmt extension for UI to work", t, func(c C) {
|
||||
Convey("Test enabling UI extension with all prerequisites", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
@@ -1172,7 +1171,165 @@ func TestValidateExtensionsConfig(t *testing.T) {
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test extension are implicitly enabled", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{
|
||||
"storage": {
|
||||
"rootDirectory": "%/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {},
|
||||
"search": {},
|
||||
"metrics": {},
|
||||
"trust": {},
|
||||
"scrub": {}
|
||||
}
|
||||
}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
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 TestApiKeyConfig(t *testing.T) {
|
||||
Convey("Test API Keys are enabled if OpenID is enabled", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex",
|
||||
"clientid":"client_id","scopes":["openid"]}}}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.HTTP.Auth, ShouldNotBeNil)
|
||||
So(config.HTTP.Auth.APIKey, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test API Keys are not enabled by default", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot"},
|
||||
"log":{"level":"debug"}}`)
|
||||
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.HTTP.Auth, ShouldNotBeNil)
|
||||
So(config.HTTP.Auth.APIKey, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Test API Keys are not enabled if OpenID is not enabled", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.HTTP.Auth, ShouldNotBeNil)
|
||||
So(config.HTTP.Auth.APIKey, ShouldBeFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeAPIKey(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("apikey implicitly enabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s",
|
||||
"auth": {
|
||||
"apikey": true
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
}
|
||||
}`
|
||||
|
||||
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), ShouldContainSubstring, "\"APIKey\":true")
|
||||
})
|
||||
|
||||
Convey("apikey disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s",
|
||||
"auth": {
|
||||
"apikey": false
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
}
|
||||
}`
|
||||
|
||||
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), ShouldContainSubstring, "\"APIKey\":false")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1557,3 +1714,44 @@ func TestScrub(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// run cli and return output.
|
||||
func runCLIWithConfig(tempDir string, config string) (string, error) {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
|
||||
logFile, err := os.CreateTemp(tempDir, "zot-log*.txt")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cfgfile, err := os.CreateTemp(tempDir, "zot-test*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config = fmt.Sprintf(config, tempDir, port, logFile.Name())
|
||||
|
||||
_, err = cfgfile.Write([]byte(config))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cfgfile.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
os.Args = []string{"cli_test", "serve", cfgfile.Name()}
|
||||
|
||||
go func() {
|
||||
err = cli.NewServerRootCmd().Execute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
WaitTillServerReady(baseURL)
|
||||
|
||||
return logFile.Name(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user