0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

config: support multiple storage locations

added support to point multiple storage locations in zot by running multiple instance of zot in background.

see examples/config-multiple.json for more info about config.

Closes #181
This commit is contained in:
Shivam Mishra 2021-04-05 17:40:33 -07:00 committed by Ramkumar Chinchani
parent 9ca6eea940
commit 28974e81dc
21 changed files with 1810 additions and 164 deletions

View file

@ -23,6 +23,7 @@ https://anuvu.github.io/zot/
* Storage optimizations: * Storage optimizations:
* Automatic garbage collection of orphaned blobs * Automatic garbage collection of orphaned blobs
* Layer deduplication using hard links when content is identical * Layer deduplication using hard links when content is identical
* Serve [multiple storage paths (and backends)](./examples/config-multiple.json) using a single zot server
* Swagger based documentation * Swagger based documentation
* Single binary for _all_ the above features * Single binary for _all_ the above features
* Released under Apache 2.0 License * Released under Apache 2.0 License

View file

@ -37,4 +37,6 @@ var (
ErrScanNotSupported = errors.New("search: scanning of image media type not supported") ErrScanNotSupported = errors.New("search: scanning of image media type not supported")
ErrCLITimeout = errors.New("cli: Query timed out while waiting for results") ErrCLITimeout = errors.New("cli: Query timed out while waiting for results")
ErrDuplicateConfigName = errors.New("cli: cli config name already added") ErrDuplicateConfigName = errors.New("cli: cli config name already added")
ErrInvalidRoute = errors.New("routes: invalid route prefix")
ErrImgStoreNotFound = errors.New("routes: image store not found corresponding to given route")
) )

View file

@ -0,0 +1,37 @@
{
"version": "0.1.0-dev",
"storage": {
"rootDirectory": "/tmp/zot",
"dedupe": true,
"gc": true,
"subPaths": {
"/infra": {
"rootDirectory": "/tmp/zot1",
"dedupe": true
},
"/b": {
"rootDirectory": "/tmp/zot2",
"dedupe": true
},
"/c": {
"rootDirectory": "/tmp/zot3",
"dedupe": false
}
}
},
"http": {
"address": "127.0.0.1",
"port": "5000",
"ReadOnly": false
},
"log": {
"level": "debug"
},
"extensions": {
"search": {
"cve": {
"updateInterval": "24h"
}
}
}
}

View file

@ -0,0 +1,30 @@
{
"version": "0.1.0-dev",
"storage": {
"rootDirectory": "/tmp/zot",
"dedupe": true,
"gc": true,
"subPaths": {
"/a": {
"rootDirectory": "/tmp/zot1",
"dedupe": true
},
"/b": {
"rootDirectory": "/tmp/zot2",
"dedupe": true
},
"/c": {
"rootDirectory": "/tmp/zot3",
"dedupe": false
}
}
},
"http": {
"address": "127.0.0.1",
"port": "5000",
"ReadOnly": false
},
"log": {
"level": "debug"
}
}

11
go.mod
View file

@ -9,6 +9,7 @@ require (
github.com/aquasecurity/trivy v0.0.0-00010101000000-000000000000 github.com/aquasecurity/trivy v0.0.0-00010101000000-000000000000
github.com/briandowns/spinner v1.11.1 github.com/briandowns/spinner v1.11.1
github.com/chartmuseum/auth v0.4.0 github.com/chartmuseum/auth v0.4.0
github.com/containers/storage v1.29.0
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a
github.com/go-chi/chi v4.0.2+incompatible // indirect github.com/go-chi/chi v4.0.2+incompatible // indirect
@ -17,7 +18,7 @@ require (
github.com/google/go-containerregistry v0.0.0-20200331213917-3d03ed9b1ca2 github.com/google/go-containerregistry v0.0.0-20200331213917-3d03ed9b1ca2
github.com/gorilla/handlers v1.4.2 github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
github.com/json-iterator/go v1.1.9 github.com/json-iterator/go v1.1.10
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.1.2
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3
@ -31,14 +32,14 @@ require (
github.com/smartystreets/goconvey v1.6.4 github.com/smartystreets/goconvey v1.6.4
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.6.1 github.com/spf13/viper v1.6.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.7.0
github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e
github.com/swaggo/swag v1.6.3 github.com/swaggo/swag v1.6.3
github.com/vektah/gqlparser/v2 v2.0.1 github.com/vektah/gqlparser/v2 v2.0.1
go.etcd.io/bbolt v1.3.4 go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
gopkg.in/resty.v1 v1.12.0 gopkg.in/resty.v1 v1.12.0
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.4.0
) )
replace github.com/aquasecurity/trivy => github.com/anuvu/trivy v0.9.2-0.20200731014147-c5f97b59c172 replace github.com/aquasecurity/trivy => github.com/anuvu/trivy v0.9.2-0.20200731014147-c5f97b59c172

513
go.sum

File diff suppressed because it is too large Load diff

View file

@ -72,11 +72,18 @@ type LogConfig struct {
Output string Output string
} }
type GlobalStorageConfig struct {
RootDirectory string
Dedupe bool
GC bool
SubPaths map[string]StorageConfig
}
type Config struct { type Config struct {
Version string Version string
Commit string Commit string
BinaryType string BinaryType string
Storage StorageConfig Storage GlobalStorageConfig
HTTP HTTPConfig HTTP HTTPConfig
Log *LogConfig Log *LogConfig
Extensions *ext.ExtensionConfig Extensions *ext.ExtensionConfig
@ -87,7 +94,7 @@ func NewConfig() *Config {
Version: dspec.Version, Version: dspec.Version,
Commit: Commit, Commit: Commit,
BinaryType: BinaryType, BinaryType: BinaryType,
Storage: StorageConfig{GC: true, Dedupe: true}, Storage: GlobalStorageConfig{GC: true, Dedupe: true},
HTTP: HTTPConfig{Address: "127.0.0.1", Port: "8080"}, HTTP: HTTPConfig{Address: "127.0.0.1", Port: "8080"},
Log: &LogConfig{Level: "debug"}, Log: &LogConfig{Level: "debug"},
} }

View file

@ -7,7 +7,6 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"os"
"time" "time"
"github.com/anuvu/zot/errors" "github.com/anuvu/zot/errors"
@ -23,11 +22,11 @@ const (
) )
type Controller struct { type Controller struct {
Config *Config Config *Config
Router *mux.Router Router *mux.Router
ImageStore *storage.ImageStore StoreController storage.StoreController
Log log.Logger Log log.Logger
Server *http.Server Server *http.Server
} }
func NewController(config *Config) *Controller { func NewController(config *Config) *Controller {
@ -63,20 +62,69 @@ func (c *Controller) Run() error {
handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log), handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log),
handlers.PrintRecoveryStack(false))) handlers.PrintRecoveryStack(false)))
c.ImageStore = storage.NewImageStore(c.Config.Storage.RootDirectory, c.Config.Storage.GC,
c.Config.Storage.Dedupe, c.Log)
if c.ImageStore == nil {
// we can't proceed without at least a image store
os.Exit(1)
}
// Enable extensions if extension config is provided
if c.Config != nil && c.Config.Extensions != nil {
ext.EnableExtensions(c.Config.Extensions, c.Log, c.Config.Storage.RootDirectory)
}
c.Router = engine c.Router = engine
c.Router.UseEncodedPath() c.Router.UseEncodedPath()
c.StoreController = storage.StoreController{}
if c.Config.Storage.RootDirectory != "" {
if c.Config.Storage.Dedupe {
err := storage.ValidateHardLink(c.Config.Storage.RootDirectory)
if err != nil {
c.Log.Warn().Msg("input storage root directory filesystem does not supports hardlinking," +
"disabling dedupe functionality")
c.Config.Storage.Dedupe = false
}
}
defaultStore := storage.NewImageStore(c.Config.Storage.RootDirectory,
c.Config.Storage.GC, c.Config.Storage.Dedupe, c.Log)
c.StoreController.DefaultStore = defaultStore
// Enable extensions if extension config is provided
if c.Config != nil && c.Config.Extensions != nil {
ext.EnableExtensions(c.Config.Extensions, c.Log, c.Config.Storage.RootDirectory)
}
} else {
// we can't proceed without global storage
c.Log.Error().Err(errors.ErrImgStoreNotFound).Msg("controller: no storage config provided")
return errors.ErrImgStoreNotFound
}
if c.Config.Storage.SubPaths != nil {
if len(c.Config.Storage.SubPaths) > 0 {
subPaths := c.Config.Storage.SubPaths
subImageStore := make(map[string]*storage.ImageStore)
// creating image store per subpaths
for route, storageConfig := range subPaths {
if storageConfig.Dedupe {
err := storage.ValidateHardLink(storageConfig.RootDirectory)
if err != nil {
c.Log.Warn().Msg("input storage root directory filesystem does not supports hardlinking, " +
"disabling dedupe functionality")
storageConfig.Dedupe = false
}
}
subImageStore[route] = storage.NewImageStore(storageConfig.RootDirectory,
storageConfig.GC, storageConfig.Dedupe, c.Log)
// Enable extensions if extension config is provided
if c.Config != nil && c.Config.Extensions != nil {
ext.EnableExtensions(c.Config.Extensions, c.Log, storageConfig.RootDirectory)
}
}
c.StoreController.SubStore = subImageStore
}
}
_ = NewRouteHandler(c) _ = NewRouteHandler(c)
addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port) addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port)

View file

@ -371,6 +371,136 @@ func TestBasicAuth(t *testing.T) {
}) })
} }
func TestMultipleInstance(t *testing.T) {
Convey("Negative test zot multiple instance", t, func() {
config := api.NewConfig()
config.HTTP.Port = SecurePort1
htpasswdPath := makeHtpasswdFile()
defer os.Remove(htpasswdPath)
config.HTTP.Auth = &api.AuthConfig{
HTPasswd: api.AuthHTPasswd{
Path: htpasswdPath,
},
}
c := api.NewController(config)
err := c.Run()
So(err, ShouldEqual, errors.ErrImgStoreNotFound)
globalDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(globalDir)
subDir, err := ioutil.TempDir("/tmp", "oci-sub-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(subDir)
c.Config.Storage.RootDirectory = globalDir
subPathMap := make(map[string]api.StorageConfig)
subPathMap["/a"] = api.StorageConfig{RootDirectory: subDir}
go func() {
if err := c.Run(); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(BaseURL1)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
defer func() {
ctx := context.Background()
_ = c.Server.Shutdown(ctx)
}()
client := resty.New()
tagResponse, err := client.R().SetBasicAuth(username, passphrase).
Get(BaseURL1 + "/v2/zot-test/tags/list")
So(err, ShouldBeNil)
So(tagResponse.StatusCode(), ShouldEqual, 404)
})
Convey("Test zot multiple instance", t, func() {
config := api.NewConfig()
config.HTTP.Port = SecurePort1
htpasswdPath := makeHtpasswdFile()
defer os.Remove(htpasswdPath)
config.HTTP.Auth = &api.AuthConfig{
HTPasswd: api.AuthHTPasswd{
Path: htpasswdPath,
},
}
c := api.NewController(config)
globalDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(globalDir)
subDir, err := ioutil.TempDir("/tmp", "oci-sub-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(subDir)
c.Config.Storage.RootDirectory = globalDir
subPathMap := make(map[string]api.StorageConfig)
subPathMap["/a"] = api.StorageConfig{RootDirectory: subDir}
go func() {
// this blocks
if err := c.Run(); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(BaseURL1)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
defer func() {
ctx := context.Background()
_ = c.Server.Shutdown(ctx)
}()
// without creds, should get access error
resp, err := resty.R().Get(BaseURL1 + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
var e api.Error
err = json.Unmarshal(resp.Body(), &e)
So(err, ShouldBeNil)
// with creds, should get expected status code
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})
}
func TestTLSWithBasicAuth(t *testing.T) { func TestTLSWithBasicAuth(t *testing.T) {
Convey("Make a new controller", t, func() { Convey("Make a new controller", t, func() {
caCert, err := ioutil.ReadFile(CACert) caCert, err := ioutil.ReadFile(CACert)
@ -1820,13 +1950,13 @@ func TestParallelRequests(t *testing.T) {
{ {
srcImageName: "zot-cve-test", srcImageName: "zot-cve-test",
srcImageTag: "0.0.1", srcImageTag: "0.0.1",
destImageName: "zot-3-test", destImageName: "a/zot-3-test",
testCaseName: "Request-3", testCaseName: "Request-3",
}, },
{ {
srcImageName: "zot-cve-test", srcImageName: "zot-cve-test",
srcImageTag: "0.0.1", srcImageTag: "0.0.1",
destImageName: "zot-4-test", destImageName: "b/zot-4-test",
testCaseName: "Request-4", testCaseName: "Request-4",
}, },
{ {
@ -1881,13 +2011,13 @@ func TestParallelRequests(t *testing.T) {
{ {
srcImageName: "zot-cve-test", srcImageName: "zot-cve-test",
srcImageTag: "0.0.1", srcImageTag: "0.0.1",
destImageName: "zot-3-test", destImageName: "a/zot-3-test",
testCaseName: "Request-13", testCaseName: "Request-13",
}, },
{ {
srcImageName: "zot-cve-test", srcImageName: "zot-cve-test",
srcImageTag: "0.0.1", srcImageTag: "0.0.1",
destImageName: "zot-4-test", destImageName: "b/zot-4-test",
testCaseName: "Request-14", testCaseName: "Request-14",
}, },
} }
@ -1896,8 +2026,6 @@ func TestParallelRequests(t *testing.T) {
config.HTTP.Port = SecurePort1 config.HTTP.Port = SecurePort1
htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase)) htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase))
// defer os.Remove(htpasswdPath)
config.HTTP.Auth = &api.AuthConfig{ config.HTTP.Auth = &api.AuthConfig{
HTPasswd: api.AuthHTPasswd{ HTPasswd: api.AuthHTPasswd{
Path: htpasswdPath, Path: htpasswdPath,
@ -1911,11 +2039,23 @@ func TestParallelRequests(t *testing.T) {
panic(err) panic(err)
} }
err = copyFiles("../../test/data", dir) firstSubDir, err := ioutil.TempDir("", "oci-sub-dir")
if err != nil { if err != nil {
panic(err) panic(err)
} }
//defer os.RemoveAll(dir)
secondSubDir, err := ioutil.TempDir("", "oci-sub-dir")
if err != nil {
panic(err)
}
subPaths := make(map[string]api.StorageConfig)
subPaths["/a"] = api.StorageConfig{RootDirectory: firstSubDir}
subPaths["/b"] = api.StorageConfig{RootDirectory: secondSubDir}
c.Config.Storage.SubPaths = subPaths
c.Config.Storage.RootDirectory = dir c.Config.Storage.RootDirectory = dir
go func() { go func() {
@ -1939,7 +2079,7 @@ func TestParallelRequests(t *testing.T) {
for i, testcase := range testCases { for i, testcase := range testCases {
testcase := testcase testcase := testcase
j := i j := i
//println(i)
t.Run(testcase.testCaseName, func(t *testing.T) { t.Run(testcase.testCaseName, func(t *testing.T) {
t.Parallel() t.Parallel()
client := resty.New() client := resty.New()
@ -1949,7 +2089,7 @@ func TestParallelRequests(t *testing.T) {
assert.Equal(t, err, nil, "Error should be nil") assert.Equal(t, err, nil, "Error should be nil")
assert.NotEqual(t, tagResponse.StatusCode(), 400, "bad request") assert.NotEqual(t, tagResponse.StatusCode(), 400, "bad request")
manifestList := getAllManifests(path.Join(c.Config.Storage.RootDirectory, testcase.srcImageName)) manifestList := getAllManifests(path.Join("../../test/data", testcase.srcImageName))
for _, manifest := range manifestList { for _, manifest := range manifestList {
headResponse, err := client.R().SetBasicAuth(username, passphrase). headResponse, err := client.R().SetBasicAuth(username, passphrase).
@ -1963,7 +2103,7 @@ func TestParallelRequests(t *testing.T) {
assert.Equal(t, getResponse.StatusCode(), 404, "response status code should return 404") assert.Equal(t, getResponse.StatusCode(), 404, "response status code should return 404")
} }
blobList := getAllBlobs(path.Join(c.Config.Storage.RootDirectory, testcase.srcImageName)) blobList := getAllBlobs(path.Join("../../test/data", testcase.srcImageName))
for _, blob := range blobList { for _, blob := range blobList {
// Get request of blob // Get request of blob
@ -1981,7 +2121,7 @@ func TestParallelRequests(t *testing.T) {
assert.Equal(t, err, nil, "Should not be nil") assert.Equal(t, err, nil, "Should not be nil")
assert.NotEqual(t, getResponse.StatusCode(), 500, "internal server error should not occurred") assert.NotEqual(t, getResponse.StatusCode(), 500, "internal server error should not occurred")
blobPath := path.Join(c.Config.Storage.RootDirectory, testcase.srcImageName, "blobs/sha256", blob) blobPath := path.Join("../../test/data", testcase.srcImageName, "blobs/sha256", blob)
buf, err := ioutil.ReadFile(blobPath) buf, err := ioutil.ReadFile(blobPath)
if err != nil { if err != nil {
@ -2050,9 +2190,6 @@ func TestParallelRequests(t *testing.T) {
SetHeader("Content-Range", fmt.Sprintf("%d", readContent)+"-"+fmt.Sprintf("%d", readContent+n-1)). SetHeader("Content-Range", fmt.Sprintf("%d", readContent)+"-"+fmt.Sprintf("%d", readContent+n-1)).
SetBasicAuth(username, passphrase). SetBasicAuth(username, passphrase).
Patch(BaseURL1 + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID) Patch(BaseURL1 + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
if err != nil {
panic(err)
}
assert.Equal(t, err, nil, "Error should be nil") assert.Equal(t, err, nil, "Error should be nil")
assert.NotEqual(t, patchResponse.StatusCode(), 500, "response status code should not return 500") assert.NotEqual(t, patchResponse.StatusCode(), 500, "response status code should not return 500")
@ -2273,3 +2410,79 @@ func stopServer(ctrl *api.Controller) {
panic(err) panic(err)
} }
} }
func TestHardLink(t *testing.T) {
Convey("Validate hard link", t, func() {
config := api.NewConfig()
config.HTTP.Port = SecurePort1
htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase))
config.HTTP.Auth = &api.AuthConfig{
HTPasswd: api.AuthHTPasswd{
Path: htpasswdPath,
},
}
c := api.NewController(config)
dir, err := ioutil.TempDir("", "hard-link-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
err = os.Chmod(dir, 0400)
if err != nil {
panic(err)
}
subDir, err := ioutil.TempDir("", "sub-hardlink-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(subDir)
err = os.Chmod(subDir, 0400)
if err != nil {
panic(err)
}
c.Config.Storage.RootDirectory = dir
subPaths := make(map[string]api.StorageConfig)
subPaths["/a"] = api.StorageConfig{RootDirectory: subDir, Dedupe: true}
c.Config.Storage.SubPaths = subPaths
go func() {
// this blocks
if err := c.Run(); err != nil {
return
}
}()
time.Sleep(5 * time.Second)
// wait till ready
for {
_, err := resty.R().Get(BaseURL1)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
err = os.Chmod(dir, 0644)
if err != nil {
panic(err)
}
err = os.Chmod(subDir, 0644)
if err != nil {
panic(err)
}
So(c.Config.Storage.Dedupe, ShouldEqual, false)
})
}

View file

@ -25,6 +25,7 @@ import (
"github.com/anuvu/zot/errors" "github.com/anuvu/zot/errors"
ext "github.com/anuvu/zot/pkg/extensions" ext "github.com/anuvu/zot/pkg/extensions"
"github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/log"
"github.com/anuvu/zot/pkg/storage"
"github.com/gorilla/mux" "github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -90,7 +91,7 @@ func (rh *RouteHandler) SetupRoutes() {
rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler) rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler)
// Setup Extensions Routes // Setup Extensions Routes
if rh.c.Config != nil && rh.c.Config.Extensions != nil { if rh.c.Config != nil && rh.c.Config.Extensions != nil {
ext.SetupRoutes(rh.c.Router, rh.c.Config.Storage.RootDirectory, rh.c.ImageStore, rh.c.Log) ext.SetupRoutes(rh.c.Router, rh.c.StoreController, rh.c.Log)
} }
} }
@ -139,6 +140,7 @@ type ImageTags struct {
// @Failure 400 {string} string "bad request". // @Failure 400 {string} string "bad request".
func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) { func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
name, ok := vars["name"] name, ok := vars["name"]
if !ok || name == "" { if !ok || name == "" {
@ -146,11 +148,11 @@ func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
paginate := false paginate := false
n := -1 n := -1
var err error
nQuery, ok := r.URL.Query()["n"] nQuery, ok := r.URL.Query()["n"]
if ok { if ok {
@ -161,6 +163,8 @@ func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) {
var n1 int64 var n1 int64
var err error
if n1, err = strconv.ParseInt(nQuery[0], 10, 0); err != nil { if n1, err = strconv.ParseInt(nQuery[0], 10, 0); err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
@ -182,7 +186,7 @@ func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) {
last = lastQuery[0] last = lastQuery[0]
} }
tags, err := rh.c.ImageStore.GetImageTags(name) tags, err := is.GetImageTags(name)
if err != nil { if err != nil {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
return return
@ -255,13 +259,15 @@ func (rh *RouteHandler) CheckManifest(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
reference, ok := vars["reference"] reference, ok := vars["reference"]
if !ok || reference == "" { if !ok || reference == "" {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))
return return
} }
_, digest, _, err := rh.c.ImageStore.GetImageManifest(name, reference) _, digest, _, err := is.GetImageManifest(name, reference)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
@ -310,13 +316,15 @@ func (rh *RouteHandler) GetManifest(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
reference, ok := vars["reference"] reference, ok := vars["reference"]
if !ok || reference == "" { if !ok || reference == "" {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})))
return return
} }
content, digest, mediaType, err := rh.c.ImageStore.GetImageManifest(name, reference) content, digest, mediaType, err := is.GetImageManifest(name, reference)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
@ -362,6 +370,8 @@ func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
reference, ok := vars["reference"] reference, ok := vars["reference"]
if !ok || reference == "" { if !ok || reference == "" {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))
@ -382,7 +392,7 @@ func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) {
return return
} }
digest, err := rh.c.ImageStore.PutImageManifest(name, reference, mediaType, body) digest, err := is.PutImageManifest(name, reference, mediaType, body)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
@ -428,13 +438,16 @@ func (rh *RouteHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
reference, ok := vars["reference"] reference, ok := vars["reference"]
if !ok || reference == "" { if !ok || reference == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
err := rh.c.ImageStore.DeleteImageManifest(name, reference) err := is.DeleteImageManifest(name, reference)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
@ -476,6 +489,8 @@ func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
digest, ok := vars["digest"] digest, ok := vars["digest"]
if !ok || digest == "" { if !ok || digest == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -484,7 +499,7 @@ func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) {
mediaType := r.Header.Get("Accept") mediaType := r.Header.Get("Accept")
ok, blen, err := rh.c.ImageStore.CheckBlob(name, digest, mediaType) ok, blen, err := is.CheckBlob(name, digest, mediaType)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
@ -530,6 +545,8 @@ func (rh *RouteHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
digest, ok := vars["digest"] digest, ok := vars["digest"]
if !ok || digest == "" { if !ok || digest == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -538,7 +555,7 @@ func (rh *RouteHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
mediaType := r.Header.Get("Accept") mediaType := r.Header.Get("Accept")
br, blen, err := rh.c.ImageStore.GetBlob(name, digest, mediaType) br, blen, err := is.GetBlob(name, digest, mediaType)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
@ -576,16 +593,20 @@ func (rh *RouteHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) {
if !ok || name == "" { if !ok || name == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
digest, ok := vars["digest"] digest, ok := vars["digest"]
if !ok || digest == "" { if !ok || digest == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
err := rh.c.ImageStore.DeleteBlob(name, digest) is := rh.getImageStore(name)
err := is.DeleteBlob(name, digest)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
@ -626,6 +647,9 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
is := rh.getImageStore(name)
// currently zot does not support cross-repository mounting, following dist-spec and returning 202
if mountDigests, ok := r.URL.Query()["mount"]; ok { if mountDigests, ok := r.URL.Query()["mount"]; ok {
if len(mountDigests) != 1 { if len(mountDigests) != 1 {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@ -639,9 +663,9 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request)
} }
// zot does not support cross mounting directly and do a workaround by copying blob using hard link // zot does not support cross mounting directly and do a workaround by copying blob using hard link
err := rh.c.ImageStore.MountBlob(name, from[0], mountDigests[0]) err := is.MountBlob(name, from[0], mountDigests[0])
if err != nil { if err != nil {
u, err := rh.c.ImageStore.NewBlobUpload(name) u, err := is.NewBlobUpload(name)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
@ -703,7 +727,7 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
sessionID, size, err := rh.c.ImageStore.FullBlobUpload(name, r.Body, digest) sessionID, size, err := is.FullBlobUpload(name, r.Body, digest)
if err != nil { if err != nil {
rh.c.Log.Error().Err(err).Int64("actual", size).Int64("expected", contentLength).Msg("failed full upload") rh.c.Log.Error().Err(err).Int64("actual", size).Int64("expected", contentLength).Msg("failed full upload")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -725,7 +749,7 @@ func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
u, err := rh.c.ImageStore.NewBlobUpload(name) u, err := is.NewBlobUpload(name)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
@ -765,13 +789,16 @@ func (rh *RouteHandler) GetBlobUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
is := rh.getImageStore(name)
sessionID, ok := vars["session_id"] sessionID, ok := vars["session_id"]
if !ok || sessionID == "" { if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
size, err := rh.c.ImageStore.GetBlobUpload(name, sessionID) size, err := is.GetBlobUpload(name, sessionID)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadUploadRange: case errors.ErrBadUploadRange:
@ -824,19 +851,21 @@ func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
is := rh.getImageStore(name)
sessionID, ok := vars["session_id"] sessionID, ok := vars["session_id"]
if !ok || sessionID == "" { if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
var err error
var clen int64 var clen int64
var err error
if r.Header.Get("Content-Length") == "" || r.Header.Get("Content-Range") == "" { if r.Header.Get("Content-Length") == "" || r.Header.Get("Content-Range") == "" {
// streamed blob upload // streamed blob upload
clen, err = rh.c.ImageStore.PutBlobChunkStreamed(name, sessionID, r.Body) clen, err = is.PutBlobChunkStreamed(name, sessionID, r.Body)
} else { } else {
// chunked blob upload // chunked blob upload
@ -863,7 +892,7 @@ func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
clen, err = rh.c.ImageStore.PutBlobChunk(name, sessionID, from, to, r.Body) clen, err = is.PutBlobChunk(name, sessionID, from, to, r.Body)
} }
if err != nil { if err != nil {
@ -916,6 +945,8 @@ func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
is := rh.getImageStore(name)
sessionID, ok := vars["session_id"] sessionID, ok := vars["session_id"]
if !ok || sessionID == "" { if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -969,7 +1000,7 @@ func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
_, err = rh.c.ImageStore.PutBlobChunk(name, sessionID, from, to, r.Body) _, err = is.PutBlobChunk(name, sessionID, from, to, r.Body)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadUploadRange: case errors.ErrBadUploadRange:
@ -992,7 +1023,7 @@ func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request)
finish: finish:
// blob chunks already transferred, just finish // blob chunks already transferred, just finish
if err := rh.c.ImageStore.FinishBlobUpload(name, sessionID, r.Body, digest); err != nil { if err := is.FinishBlobUpload(name, sessionID, r.Body, digest); err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
WriteJSON(w, http.StatusBadRequest, WriteJSON(w, http.StatusBadRequest,
@ -1040,13 +1071,15 @@ func (rh *RouteHandler) DeleteBlobUpload(w http.ResponseWriter, r *http.Request)
return return
} }
is := rh.getImageStore(name)
sessionID, ok := vars["session_id"] sessionID, ok := vars["session_id"]
if !ok || sessionID == "" { if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
if err := rh.c.ImageStore.DeleteBlobUpload(name, sessionID); err != nil { if err := is.DeleteBlobUpload(name, sessionID); err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound, WriteJSON(w, http.StatusNotFound,
@ -1078,13 +1111,32 @@ type RepositoryList struct {
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/_catalog [get]. // @Router /v2/_catalog [get].
func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request) { func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request) {
repos, err := rh.c.ImageStore.GetRepositories() combineRepoList := make([]string, 0)
if err != nil {
w.WriteHeader(http.StatusInternalServerError) subStore := rh.c.StoreController.SubStore
return
for _, imgStore := range subStore {
repos, err := imgStore.GetRepositories()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
combineRepoList = append(combineRepoList, repos...)
} }
is := RepositoryList{Repositories: repos} singleStore := rh.c.StoreController.DefaultStore
if singleStore != nil {
repos, err := singleStore.GetRepositories()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
combineRepoList = append(combineRepoList, repos...)
}
is := RepositoryList{Repositories: combineRepoList}
WriteJSON(w, http.StatusOK, is) WriteJSON(w, http.StatusOK, is)
} }
@ -1148,3 +1200,8 @@ func WriteDataFromReader(w http.ResponseWriter, status int, length int64, mediaT
} }
} }
} }
// will return image storage corresponding to subpath provided in config.
func (rh *RouteHandler) getImageStore(name string) *storage.ImageStore {
return rh.c.StoreController.GetImageStore(name)
}

View file

@ -1,11 +1,12 @@
package compliance package compliance
type Config struct { type Config struct {
Address string Address string
Port string Port string
Version string Version string
OutputJSON bool StorageInfo []string
Compliance bool OutputJSON bool
Compliance bool
} }
func NewConfig() *Config { func NewConfig() *Config {

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"strings" "strings"
"testing" "testing"
@ -46,6 +47,8 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) {
baseURL := fmt.Sprintf("http://%s:%s", config.Address, config.Port) baseURL := fmt.Sprintf("http://%s:%s", config.Address, config.Port)
storageInfo := config.StorageInfo
fmt.Println("------------------------------") fmt.Println("------------------------------")
fmt.Println("Checking for v1.0.0 compliance") fmt.Println("Checking for v1.0.0 compliance")
fmt.Println("------------------------------") fmt.Println("------------------------------")
@ -459,6 +462,11 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) {
loc := Location(baseURL, resp) loc := Location(baseURL, resp)
So(loc, ShouldNotBeEmpty) So(loc, ShouldNotBeEmpty)
// since we are not specifying any prefix i.e provided in config while starting server,
// so it should store repo7 to global root dir
_, err = os.Stat(path.Join(storageInfo[0], "repo7"))
So(err, ShouldBeNil)
resp, err = resty.R().Get(loc) resp, err = resty.R().Get(loc)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 204) So(resp.StatusCode(), ShouldEqual, 204)
@ -686,6 +694,276 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202) So(resp.StatusCode(), ShouldEqual, 202)
}) })
Convey("Multiple Storage", func() {
// test APIS on subpath routes, default storage already tested above
// subpath route firsttest
resp, err := resty.R().Post(baseURL + "/v2/firsttest/first/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
firstloc := Location(baseURL, resp)
So(firstloc, ShouldNotBeEmpty)
resp, err = resty.R().Get(firstloc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 204)
// if firsttest route is used as prefix in url that means repo should be stored in subpaths["firsttest"] rootdir
_, err = os.Stat(path.Join(storageInfo[1], "firsttest/first"))
So(err, ShouldBeNil)
// subpath route secondtest
resp, err = resty.R().Post(baseURL + "/v2/secondtest/second/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
secondloc := Location(baseURL, resp)
So(secondloc, ShouldNotBeEmpty)
resp, err = resty.R().Get(secondloc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 204)
// if secondtest route is used as prefix in url that means repo should be stored in subpaths["secondtest"] rootdir
_, err = os.Stat(path.Join(storageInfo[2], "secondtest/second"))
So(err, ShouldBeNil)
content := []byte("this is a blob5")
digest := godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
// monolithic blob upload: success
// first test
resp, err = resty.R().SetQueryParam("digest", digest.String()).
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(firstloc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
firstblobLoc := resp.Header().Get("Location")
So(firstblobLoc, ShouldNotBeEmpty)
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
// second test
resp, err = resty.R().SetQueryParam("digest", digest.String()).
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(secondloc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
secondblobLoc := resp.Header().Get("Location")
So(secondblobLoc, ShouldNotBeEmpty)
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
// check a non-existent manifest
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Head(baseURL + "/v2/firsttest/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Head(baseURL + "/v2/secondtest/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
// create a manifest
m := ispec.Manifest{
Config: ispec.Descriptor{
Digest: digest,
Size: int64(len(content)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(len(content)),
},
},
}
m.SchemaVersion = 2
content, err = json.Marshal(m)
So(err, ShouldBeNil)
digest = godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
// subpath firsttest
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Put(baseURL + "/v2/firsttest/first/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
d := resp.Header().Get(api.DistContentDigestKey)
So(d, ShouldNotBeEmpty)
So(d, ShouldEqual, digest.String())
// subpath secondtest
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Put(baseURL + "/v2/secondtest/second/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
d = resp.Header().Get(api.DistContentDigestKey)
So(d, ShouldNotBeEmpty)
So(d, ShouldEqual, digest.String())
content = []byte("this is a blob5")
digest = godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
// create a manifest with same blob but a different tag
m = ispec.Manifest{
Config: ispec.Descriptor{
Digest: digest,
Size: int64(len(content)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(len(content)),
},
},
}
m.SchemaVersion = 2
content, err = json.Marshal(m)
So(err, ShouldBeNil)
digest = godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
// subpath firsttest
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Put(baseURL + "/v2/firsttest/first/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
d = resp.Header().Get(api.DistContentDigestKey)
So(d, ShouldNotBeEmpty)
So(d, ShouldEqual, digest.String())
// subpath secondtest
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Put(baseURL + "/v2/secondtest/second/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
d = resp.Header().Get(api.DistContentDigestKey)
So(d, ShouldNotBeEmpty)
So(d, ShouldEqual, digest.String())
// check/get by tag
resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeEmpty)
// check/get by reference
resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeEmpty)
// delete manifest by tag should fail
resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400)
resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400)
// delete manifest by digest
resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
// delete manifest by digest
resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
// delete again should fail
resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
// check/get by tag
resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
So(resp.Body(), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
So(resp.Body(), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/repo7/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
So(resp.Body(), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
So(resp.Body(), ShouldNotBeEmpty)
// check/get by reference
resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
So(resp.Body(), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
So(resp.Body(), ShouldNotBeEmpty)
})
}) })
} }

View file

@ -18,24 +18,35 @@ import (
// nolint: gochecknoglobals // nolint: gochecknoglobals
var ( var (
listenAddress = "127.0.0.1" listenAddress = "127.0.0.1"
defaultDir = ""
firstDir = ""
secondDir = ""
) )
func TestWorkflows(t *testing.T) { func TestWorkflows(t *testing.T) {
ctrl, randomPort := startServer() ctrl, randomPort := startServer()
defer stopServer(ctrl) defer stopServer(ctrl)
storageInfo := []string{defaultDir, firstDir, secondDir}
v1_0_0.CheckWorkflows(t, &compliance.Config{ v1_0_0.CheckWorkflows(t, &compliance.Config{
Address: listenAddress, Address: listenAddress,
Port: randomPort, Port: randomPort,
StorageInfo: storageInfo,
}) })
} }
func TestWorkflowsOutputJSON(t *testing.T) { func TestWorkflowsOutputJSON(t *testing.T) {
ctrl, randomPort := startServer() ctrl, randomPort := startServer()
defer stopServer(ctrl) defer stopServer(ctrl)
storageInfo := []string{defaultDir, firstDir, secondDir}
v1_0_0.CheckWorkflows(t, &compliance.Config{ v1_0_0.CheckWorkflows(t, &compliance.Config{
Address: listenAddress, Address: listenAddress,
Port: randomPort, Port: randomPort,
OutputJSON: true, OutputJSON: true,
StorageInfo: storageInfo,
}) })
} }
@ -59,8 +70,31 @@ func startServer() (*api.Controller, string) {
panic(err) panic(err)
} }
defaultDir = dir
firstSubDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
panic(err)
}
firstDir = firstSubDir
secondSubDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
panic(err)
}
secondDir = secondSubDir
subPaths := make(map[string]api.StorageConfig)
subPaths["/firsttest"] = api.StorageConfig{RootDirectory: firstSubDir}
subPaths["/secondtest"] = api.StorageConfig{RootDirectory: secondSubDir}
ctrl.Config.Storage.RootDirectory = dir ctrl.Config.Storage.RootDirectory = dir
ctrl.Config.Storage.SubPaths = subPaths
go func() { go func() {
// this blocks // this blocks
if err := ctrl.Run(); err != nil { if err := ctrl.Run(); err != nil {

View file

@ -55,9 +55,9 @@ func EnableExtensions(extension *ExtensionConfig, log log.Logger, rootDir string
} }
// SetupRoutes ... // SetupRoutes ...
func SetupRoutes(router *mux.Router, rootDir string, imgStore *storage.ImageStore, log log.Logger) { func SetupRoutes(router *mux.Router, storeController storage.StoreController, log log.Logger) {
log.Info().Msg("setting up extensions routes") log.Info().Msg("setting up extensions routes")
resConfig := search.GetResolverConfig(rootDir, log, imgStore) resConfig := search.GetResolverConfig(log, storeController)
router.PathPrefix("/query").Methods("GET", "POST"). router.PathPrefix("/query").Methods("GET", "POST").
Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig))) Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig)))
} }

View file

@ -21,6 +21,6 @@ func EnableExtensions(extension *ExtensionConfig, log log.Logger, rootDir string
} }
// SetupRoutes ... // SetupRoutes ...
func SetupRoutes(router *mux.Router, rootDir string, imgStore *storage.ImageStore, log log.Logger) { func SetupRoutes(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") 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")
} }

View file

@ -2,6 +2,7 @@ package cveinfo
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@ -10,6 +11,7 @@ import (
"github.com/anuvu/zot/errors" "github.com/anuvu/zot/errors"
"github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/log"
"github.com/anuvu/zot/pkg/storage"
integration "github.com/aquasecurity/trivy/integration" integration "github.com/aquasecurity/trivy/integration"
config "github.com/aquasecurity/trivy/integration/config" config "github.com/aquasecurity/trivy/integration/config"
"github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/trivy/pkg/report"
@ -44,6 +46,84 @@ func ScanImage(config *config.Config) (report.Results, error) {
return integration.ScanTrivyImage(config.TrivyConfig) return integration.ScanTrivyImage(config.TrivyConfig)
} }
func GetCVEInfo(storeController storage.StoreController, log log.Logger) (*CveInfo, error) {
cveController := CveTrivyController{}
subCveConfig := make(map[string]*config.Config)
if storeController.DefaultStore != nil {
imageStore := storeController.DefaultStore
rootDir := imageStore.RootDir()
config, err := NewTrivyConfig(rootDir)
if err != nil {
return nil, err
}
cveController.DefaultCveConfig = config
}
if storeController.SubStore != nil {
for route, storage := range storeController.SubStore {
rootDir := storage.RootDir()
config, err := NewTrivyConfig(rootDir)
if err != nil {
return nil, err
}
subCveConfig[route] = config
}
}
cveController.SubCveConfig = subCveConfig
return &CveInfo{Log: log, CveTrivyController: cveController, StoreController: storeController}, nil
}
func getRoutePrefix(name string) string {
names := strings.SplitN(name, "/", 2)
if len(names) != 2 { // nolint: gomnd
// it means route is of global storage e.g "centos:latest"
if len(names) == 1 {
return "/"
}
}
return fmt.Sprintf("/%s", names[0])
}
func (cveinfo CveInfo) GetTrivyConfig(image string) *config.Config {
// Split image to get route prefix
prefixName := getRoutePrefix(image)
var trivyConfig *config.Config
var ok bool
var rootDir string
// Get corresponding CVE trivy config, if no sub cve config present that means its default
trivyConfig, ok = cveinfo.CveTrivyController.SubCveConfig[prefixName]
if ok {
imgStore := cveinfo.StoreController.SubStore[prefixName]
rootDir = imgStore.RootDir()
} else {
trivyConfig = cveinfo.CveTrivyController.DefaultCveConfig
imgStore := cveinfo.StoreController.DefaultStore
rootDir = imgStore.RootDir()
}
trivyConfig.TrivyConfig.Input = path.Join(rootDir, image)
return trivyConfig
}
func (cveinfo CveInfo) IsValidImageFormat(imagePath string) (bool, error) { func (cveinfo CveInfo) IsValidImageFormat(imagePath string) (bool, error) {
imageDir, inputTag := getImageDirAndTag(imagePath) imageDir, inputTag := getImageDirAndTag(imagePath)
@ -113,16 +193,85 @@ func getImageDirAndTag(imageName string) (string, string) {
return imageDir, imageTag return imageDir, imageTag
} }
// Below method will return image path including root dir, root dir is determined by splitting.
func (cveinfo CveInfo) GetImageRepoPath(image string) string {
var rootDir string
prefixName := getRoutePrefix(image)
subStore := cveinfo.StoreController.SubStore
if subStore != nil {
imgStore, ok := cveinfo.StoreController.SubStore[prefixName]
if ok {
rootDir = imgStore.RootDir()
} else {
rootDir = cveinfo.StoreController.DefaultStore.RootDir()
}
} else {
rootDir = cveinfo.StoreController.DefaultStore.RootDir()
}
return path.Join(rootDir, image)
}
func (cveinfo CveInfo) GetImageListForCVE(repo string, id string, imgStore *storage.ImageStore,
trivyConfig *config.Config) ([]*string, error) {
tags := make([]*string, 0)
tagList, err := imgStore.GetImageTags(repo)
if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag")
return tags, err
}
rootDir := imgStore.RootDir()
for _, tag := range tagList {
trivyConfig.TrivyConfig.Input = fmt.Sprintf("%s:%s", path.Join(rootDir, repo), tag)
isValidImage, _ := cveinfo.IsValidImageFormat(trivyConfig.TrivyConfig.Input)
if !isValidImage {
cveinfo.Log.Debug().Str("image", repo+":"+tag).Msg("image media type not supported for scanning")
continue
}
cveinfo.Log.Info().Str("image", repo+":"+tag).Msg("scanning image")
results, err := ScanImage(trivyConfig)
if err != nil {
cveinfo.Log.Error().Err(err).Str("image", repo+":"+tag).Msg("unable to scan image")
continue
}
for _, result := range results {
for _, vulnerability := range result.Vulnerabilities {
if vulnerability.VulnerabilityID == id {
copyImgTag := tag
tags = append(tags, &copyImgTag)
break
}
}
}
}
return tags, nil
}
// GetImageTagsWithTimestamp returns a list of image tags with timestamp available in the specified repository. // GetImageTagsWithTimestamp returns a list of image tags with timestamp available in the specified repository.
func (cveinfo CveInfo) GetImageTagsWithTimestamp(rootDir string, repo string) ([]TagInfo, error) { func (cveinfo CveInfo) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) {
tagsInfo := make([]TagInfo, 0) tagsInfo := make([]TagInfo, 0)
dir := path.Join(rootDir, repo) imagePath := cveinfo.GetImageRepoPath(repo)
if !dirExists(dir) { if !dirExists(imagePath) {
return nil, errors.ErrRepoNotFound return nil, errors.ErrRepoNotFound
} }
manifests, err := cveinfo.getImageManifests(dir) manifests, err := cveinfo.getImageManifests(imagePath)
if err != nil { if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to read image manifests") cveinfo.Log.Error().Err(err).Msg("unable to read image manifests")
@ -135,7 +284,7 @@ func (cveinfo CveInfo) GetImageTagsWithTimestamp(rootDir string, repo string) ([
v, ok := manifest.Annotations[ispec.AnnotationRefName] v, ok := manifest.Annotations[ispec.AnnotationRefName]
if ok { if ok {
imageBlobManifest, err := cveinfo.getImageBlobManifest(dir, digest) imageBlobManifest, err := cveinfo.getImageBlobManifest(imagePath, digest)
if err != nil { if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest") cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
@ -143,7 +292,7 @@ func (cveinfo CveInfo) GetImageTagsWithTimestamp(rootDir string, repo string) ([
return tagsInfo, err return tagsInfo, err
} }
imageInfo, err := cveinfo.getImageInfo(dir, imageBlobManifest.Config.Digest) imageInfo, err := cveinfo.getImageInfo(imagePath, imageBlobManifest.Config.Digest)
if err != nil { if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to read image info") cveinfo.Log.Error().Err(err).Msg("unable to read image info")

View file

@ -16,6 +16,7 @@ import (
ext "github.com/anuvu/zot/pkg/extensions" ext "github.com/anuvu/zot/pkg/extensions"
cveinfo "github.com/anuvu/zot/pkg/extensions/search/cve" cveinfo "github.com/anuvu/zot/pkg/extensions/search/cve"
"github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/log"
"github.com/anuvu/zot/pkg/storage"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
@ -23,8 +24,9 @@ import (
// nolint:gochecknoglobals // nolint:gochecknoglobals
var ( var (
cve *cveinfo.CveInfo cve *cveinfo.CveInfo
dbDir string dbDir string
updateDuration time.Duration
) )
const ( const (
@ -76,7 +78,11 @@ func testSetup() error {
return err return err
} }
cve = &cveinfo.CveInfo{Log: log.NewLogger("debug", "")} log := log.NewLogger("debug", "")
cve = &cveinfo.CveInfo{Log: log}
cve.StoreController = storage.StoreController{DefaultStore: storage.NewImageStore(dir, false, false, log)}
dbDir = dir dbDir = dir
@ -365,6 +371,67 @@ func makeHtpasswdFile() string {
return f.Name() return f.Name()
} }
func TestMultipleStoragePath(t *testing.T) {
Convey("Test multiple storage path", t, func() {
// Create temporary directory
firstRootDir, err := ioutil.TempDir("", "util_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(firstRootDir)
secondRootDir, err := ioutil.TempDir("", "util_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(secondRootDir)
thirdRootDir, err := ioutil.TempDir("", "util_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(thirdRootDir)
log := log.NewLogger("debug", "")
// Create ImageStore
firstStore := storage.NewImageStore(firstRootDir, false, false, log)
secondStore := storage.NewImageStore(secondRootDir, false, false, log)
thirdStore := storage.NewImageStore(thirdRootDir, false, false, log)
storeController := storage.StoreController{}
storeController.DefaultStore = firstStore
subStore := make(map[string]*storage.ImageStore)
subStore["/a"] = secondStore
subStore["/b"] = thirdStore
storeController.SubStore = subStore
cveInfo, err := cveinfo.GetCVEInfo(storeController, log)
So(err, ShouldBeNil)
So(cveInfo.StoreController.DefaultStore, ShouldNotBeNil)
So(cveInfo.StoreController.SubStore, ShouldNotBeNil)
imagePath := cveInfo.GetImageRepoPath("zot-test")
So(imagePath, ShouldEqual, path.Join(firstRootDir, "zot-test"))
imagePath = cveInfo.GetImageRepoPath("a/zot-a-test")
So(imagePath, ShouldEqual, path.Join(secondRootDir, "a/zot-a-test"))
imagePath = cveInfo.GetImageRepoPath("b/zot-b-test")
So(imagePath, ShouldEqual, path.Join(thirdRootDir, "b/zot-b-test"))
imagePath = cveInfo.GetImageRepoPath("c/zot-c-test")
So(imagePath, ShouldEqual, path.Join(firstRootDir, "c/zot-c-test"))
})
}
func TestDownloadDB(t *testing.T) { func TestDownloadDB(t *testing.T) {
Convey("Download DB passing invalid dir", t, func() { Convey("Download DB passing invalid dir", t, func() {
err := testSetup() err := testSetup()
@ -425,35 +492,35 @@ func TestImageFormat(t *testing.T) {
func TestImageTag(t *testing.T) { func TestImageTag(t *testing.T) {
Convey("Test image tag", t, func() { Convey("Test image tag", t, func() {
imageTags, err := cve.GetImageTagsWithTimestamp(dbDir, "zot-test") imageTags, err := cve.GetImageTagsWithTimestamp("zot-test")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(imageTags), ShouldNotEqual, 0) So(len(imageTags), ShouldNotEqual, 0)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-tes") imageTags, err = cve.GetImageTagsWithTimestamp("zot-tes")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(imageTags, ShouldBeNil) So(imageTags, ShouldBeNil)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-noindex-test") imageTags, err = cve.GetImageTagsWithTimestamp("zot-noindex-test")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(len(imageTags), ShouldEqual, 0) So(len(imageTags), ShouldEqual, 0)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-squashfs-noblobs") imageTags, err = cve.GetImageTagsWithTimestamp("zot-squashfs-noblobs")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(len(imageTags), ShouldEqual, 0) So(len(imageTags), ShouldEqual, 0)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-squashfs-invalid-index") imageTags, err = cve.GetImageTagsWithTimestamp("zot-squashfs-invalid-index")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(len(imageTags), ShouldEqual, 0) So(len(imageTags), ShouldEqual, 0)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-squashfs-invalid-blob") imageTags, err = cve.GetImageTagsWithTimestamp("zot-squashfs-invalid-blob")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(len(imageTags), ShouldEqual, 0) So(len(imageTags), ShouldEqual, 0)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-invalid-layer") imageTags, err = cve.GetImageTagsWithTimestamp("zot-invalid-layer")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(len(imageTags), ShouldEqual, 0) So(len(imageTags), ShouldEqual, 0)
imageTags, err = cve.GetImageTagsWithTimestamp(dbDir, "zot-no-layer") imageTags, err = cve.GetImageTagsWithTimestamp("zot-no-layer")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(len(imageTags), ShouldEqual, 0) So(len(imageTags), ShouldEqual, 0)
}) })
@ -461,7 +528,7 @@ func TestImageTag(t *testing.T) {
func TestCVESearch(t *testing.T) { func TestCVESearch(t *testing.T) {
Convey("Test image vulenrability scanning", t, func() { Convey("Test image vulenrability scanning", t, func() {
updateDuration, _ := time.ParseDuration("1h") updateDuration, _ = time.ParseDuration("1h")
config := api.NewConfig() config := api.NewConfig()
config.HTTP.Port = SecurePort1 config.HTTP.Port = SecurePort1
htpasswdPath := makeHtpasswdFile() htpasswdPath := makeHtpasswdFile()
@ -473,7 +540,6 @@ func TestCVESearch(t *testing.T) {
}, },
} }
c := api.NewController(config) c := api.NewController(config)
defer os.RemoveAll(dbDir)
c.Config.Storage.RootDirectory = dbDir c.Config.Storage.RootDirectory = dbDir
cveConfig := &ext.CVEConfig{ cveConfig := &ext.CVEConfig{
UpdateInterval: updateDuration, UpdateInterval: updateDuration,
@ -698,13 +764,30 @@ func TestCVEConfig(t *testing.T) {
}, },
} }
c := api.NewController(config) c := api.NewController(config)
dir, err := ioutil.TempDir("", "oci-repo-test") firstDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer os.RemoveAll(dir)
c.Config.Storage.RootDirectory = dir secondDir, err := ioutil.TempDir("", "oci-repo-test")
c.Config.Extensions = &ext.ExtensionConfig{} if err != nil {
panic(err)
}
defer os.RemoveAll(firstDir)
defer os.RemoveAll(secondDir)
err = copyFiles("../../../../test/data", path.Join(secondDir, "a"))
if err != nil {
panic(err)
}
c.Config.Storage.RootDirectory = firstDir
subPaths := make(map[string]api.StorageConfig)
subPaths["/a"] = api.StorageConfig{
RootDirectory: secondDir,
}
c.Config.Storage.SubPaths = subPaths
go func() { go func() {
// this blocks // this blocks
if err := c.Run(); err != nil { if err := c.Run(); err != nil {
@ -721,6 +804,22 @@ func TestCVEConfig(t *testing.T) {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
resp, _ := resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/_catalog")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/a/zot-test/tags/list")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/zot-test/tags/list")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
defer func() { defer func() {
ctx := context.Background() ctx := context.Background()
_ = c.Server.Shutdown(ctx) _ = c.Server.Shutdown(ctx)

View file

@ -5,13 +5,20 @@ import (
"time" "time"
"github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/log"
"github.com/anuvu/zot/pkg/storage"
config "github.com/aquasecurity/trivy/integration/config" config "github.com/aquasecurity/trivy/integration/config"
) )
// CveInfo ... // CveInfo ...
type CveInfo struct { type CveInfo struct {
Log log.Logger Log log.Logger
CveTrivyConfig *config.Config CveTrivyController CveTrivyController
StoreController storage.StoreController
}
type CveTrivyController struct {
DefaultCveConfig *config.Config
SubCveConfig map[string]*config.Config
} }
type TagInfo struct { type TagInfo struct {

View file

@ -4,10 +4,11 @@ package search
import ( import (
"context" "context"
"path" "fmt"
"strings" "strings"
"github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/log"
"github.com/aquasecurity/trivy/integration/config"
cveinfo "github.com/anuvu/zot/pkg/extensions/search/cve" cveinfo "github.com/anuvu/zot/pkg/extensions/search/cve"
"github.com/anuvu/zot/pkg/storage" "github.com/anuvu/zot/pkg/storage"
@ -15,9 +16,8 @@ import (
// Resolver ... // Resolver ...
type Resolver struct { type Resolver struct {
cveInfo *cveinfo.CveInfo cveInfo *cveinfo.CveInfo
imgStore *storage.ImageStore storeController storage.StoreController
dir string
} }
// Query ... // Query ...
@ -35,33 +35,31 @@ type cveDetail struct {
} }
// GetResolverConfig ... // GetResolverConfig ...
func GetResolverConfig(dir string, log log.Logger, imgstorage *storage.ImageStore) Config { func GetResolverConfig(log log.Logger, storeController storage.StoreController) Config {
config, err := cveinfo.NewTrivyConfig(dir) cveInfo, err := cveinfo.GetCVEInfo(storeController, log)
if err != nil { if err != nil {
panic(err) panic(err)
} }
cve := &cveinfo.CveInfo{Log: log, CveTrivyConfig: config} resConfig := &Resolver{cveInfo: cveInfo, storeController: storeController}
resConfig := &Resolver{cveInfo: cve, imgStore: imgstorage, dir: dir}
return Config{Resolvers: resConfig, Directives: DirectiveRoot{}, return Config{Resolvers: resConfig, Directives: DirectiveRoot{},
Complexity: ComplexityRoot{}} Complexity: ComplexityRoot{}}
} }
func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*CVEResultForImage, error) { func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*CVEResultForImage, error) {
r.cveInfo.CveTrivyConfig.TrivyConfig.Input = path.Join(r.dir, image) trivyConfig := r.cveInfo.GetTrivyConfig(image)
r.cveInfo.Log.Info().Str("image", image).Msg("scanning image") r.cveInfo.Log.Info().Str("image", image).Msg("scanning image")
isValidImage, err := r.cveInfo.IsValidImageFormat(r.cveInfo.CveTrivyConfig.TrivyConfig.Input) isValidImage, err := r.cveInfo.IsValidImageFormat(trivyConfig.TrivyConfig.Input)
if !isValidImage { if !isValidImage {
r.cveInfo.Log.Debug().Str("image", image).Msg("image media type not supported for scanning") r.cveInfo.Log.Debug().Str("image", image).Msg("image media type not supported for scanning")
return &CVEResultForImage{}, err return &CVEResultForImage{}, err
} }
cveResults, err := cveinfo.ScanImage(r.cveInfo.CveTrivyConfig) cveResults, err := cveinfo.ScanImage(trivyConfig)
if err != nil { if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("unable to scan image repository") r.cveInfo.Log.Error().Err(err).Msg("unable to scan image repository")
@ -134,62 +132,70 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*CVE
} }
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*ImgResultForCve, error) { func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*ImgResultForCve, error) {
cveResult := []*ImgResultForCve{} finalCveResult := []*ImgResultForCve{}
r.cveInfo.Log.Info().Msg("extracting repositories") r.cveInfo.Log.Info().Msg("extracting repositories")
repoList, err := r.imgStore.GetRepositories() defaultStore := r.storeController.DefaultStore
defaultTrivyConfig := r.cveInfo.CveTrivyController.DefaultCveConfig
repoList, err := defaultStore.GetRepositories()
if err != nil { if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("unable to search repositories") r.cveInfo.Log.Error().Err(err).Msg("unable to search repositories")
return cveResult, err return finalCveResult, err
} }
r.cveInfo.Log.Info().Msg("scanning each repository") r.cveInfo.Log.Info().Msg("scanning each global repository")
cveResult, err := r.getImageListForCVE(repoList, id, defaultStore, defaultTrivyConfig)
if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("error getting cve list for global repositories")
return finalCveResult, err
}
finalCveResult = append(finalCveResult, cveResult...)
subStore := r.storeController.SubStore
for route, store := range subStore {
subRepoList, err := store.GetRepositories()
if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("unable to search repositories")
return cveResult, err
}
subTrivyConfig := r.cveInfo.CveTrivyController.SubCveConfig[route]
subCveResult, err := r.getImageListForCVE(subRepoList, id, store, subTrivyConfig)
if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("unable to get cve result for sub repositories")
return finalCveResult, err
}
finalCveResult = append(finalCveResult, subCveResult...)
}
return finalCveResult, nil
}
func (r *queryResolver) getImageListForCVE(repoList []string, id string, imgStore *storage.ImageStore,
trivyConfig *config.Config) ([]*ImgResultForCve, error) {
cveResult := []*ImgResultForCve{}
for _, repo := range repoList { for _, repo := range repoList {
r.cveInfo.Log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo") r.cveInfo.Log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
tagList, err := r.imgStore.GetImageTags(repo) name := repo
tags, err := r.cveInfo.GetImageListForCVE(repo, id, imgStore, trivyConfig)
if err != nil { if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("unable to get list of image tag") r.cveInfo.Log.Error().Err(err).Msg("error getting tag")
}
var name string return cveResult, err
tags := make([]*string, 0)
for _, tag := range tagList {
r.cveInfo.CveTrivyConfig.TrivyConfig.Input = path.Join(r.dir, repo+":"+tag)
isValidImage, _ := r.cveInfo.IsValidImageFormat(r.cveInfo.CveTrivyConfig.TrivyConfig.Input)
if !isValidImage {
r.cveInfo.Log.Debug().Str("image", repo+":"+tag).Msg("image media type not supported for scanning")
continue
}
r.cveInfo.Log.Info().Str("image", repo+":"+tag).Msg("scanning image")
results, err := cveinfo.ScanImage(r.cveInfo.CveTrivyConfig)
if err != nil {
r.cveInfo.Log.Error().Err(err).Str("image", repo+":"+tag).Msg("unable to scan image")
continue
}
name = repo
for _, result := range results {
for _, vulnerability := range result.Vulnerabilities {
if vulnerability.VulnerabilityID == id {
copyImgTag := tag
tags = append(tags, &copyImgTag)
break
}
}
}
} }
if len(tags) != 0 { if len(tags) != 0 {
@ -203,9 +209,17 @@ func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*ImgR
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) (*ImgResultForFixedCve, error) { // nolint: lll func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) (*ImgResultForFixedCve, error) { // nolint: lll
imgResultForFixedCVE := &ImgResultForFixedCve{} imgResultForFixedCVE := &ImgResultForFixedCve{}
r.cveInfo.Log.Info().Str("image", image).Msg("retrieving image path")
imagePath := r.cveInfo.GetImageRepoPath(image)
r.cveInfo.Log.Info().Str("image", image).Msg("retrieving trivy config")
trivyConfig := r.cveInfo.GetTrivyConfig(image)
r.cveInfo.Log.Info().Str("image", image).Msg("extracting list of tags available in image") r.cveInfo.Log.Info().Str("image", image).Msg("extracting list of tags available in image")
tagsInfo, err := r.cveInfo.GetImageTagsWithTimestamp(r.dir, image) tagsInfo, err := r.cveInfo.GetImageTagsWithTimestamp(image)
if err != nil { if err != nil {
r.cveInfo.Log.Error().Err(err).Msg("unable to read image tags") r.cveInfo.Log.Error().Err(err).Msg("unable to read image tags")
@ -217,9 +231,9 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
var hasCVE bool var hasCVE bool
for _, tag := range tagsInfo { for _, tag := range tagsInfo {
r.cveInfo.CveTrivyConfig.TrivyConfig.Input = path.Join(r.dir, image+":"+tag.Name) trivyConfig.TrivyConfig.Input = fmt.Sprintf("%s:%s", imagePath, tag.Name)
isValidImage, _ := r.cveInfo.IsValidImageFormat(r.cveInfo.CveTrivyConfig.TrivyConfig.Input) isValidImage, _ := r.cveInfo.IsValidImageFormat(trivyConfig.TrivyConfig.Input)
if !isValidImage { if !isValidImage {
r.cveInfo.Log.Debug().Str("image", r.cveInfo.Log.Debug().Str("image",
image+":"+tag.Name).Msg("image media type not supported for scanning, adding as an infected image") image+":"+tag.Name).Msg("image media type not supported for scanning, adding as an infected image")
@ -231,7 +245,7 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
r.cveInfo.Log.Info().Str("image", image+":"+tag.Name).Msg("scanning image") r.cveInfo.Log.Info().Str("image", image+":"+tag.Name).Msg("scanning image")
results, err := cveinfo.ScanImage(r.cveInfo.CveTrivyConfig) results, err := cveinfo.ScanImage(trivyConfig)
if err != nil { if err != nil {
r.cveInfo.Log.Error().Err(err).Str("image", image+":"+tag.Name).Msg("unable to scan image") r.cveInfo.Log.Error().Err(err).Str("image", image+":"+tag.Name).Msg("unable to scan image")

View file

@ -10,6 +10,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
@ -37,6 +38,11 @@ type BlobUpload struct {
ID string ID string
} }
type StoreController struct {
DefaultStore *ImageStore
SubStore map[string]*ImageStore
}
// ImageStore provides the image storage operations. // ImageStore provides the image storage operations.
type ImageStore struct { type ImageStore struct {
rootDir string rootDir string
@ -48,6 +54,39 @@ type ImageStore struct {
log zerolog.Logger log zerolog.Logger
} }
func (is *ImageStore) RootDir() string {
return is.rootDir
}
func getRoutePrefix(name string) string {
names := strings.SplitN(name, "/", 2)
if len(names) != 2 { // nolint: gomnd
// it means route is of global storage e.g "centos:latest"
if len(names) == 1 {
return "/"
}
}
return fmt.Sprintf("/%s", names[0])
}
func (sc StoreController) GetImageStore(name string) *ImageStore {
if sc.SubStore != nil {
// SubStore is being provided, now we need to find equivalent image store and this will be found by splitting name
prefixName := getRoutePrefix(name)
imgStore, ok := sc.SubStore[prefixName]
if !ok {
imgStore = sc.DefaultStore
}
return imgStore
}
return sc.DefaultStore
}
// NewImageStore returns a new image store backed by a file storage. // NewImageStore returns a new image store backed by a file storage.
func NewImageStore(rootDir string, gc bool, dedupe bool, log zlog.Logger) *ImageStore { func NewImageStore(rootDir string, gc bool, dedupe bool, log zlog.Logger) *ImageStore {
if _, err := os.Stat(rootDir); os.IsNotExist(err) { if _, err := os.Stat(rootDir); os.IsNotExist(err) {
@ -1174,6 +1213,36 @@ func Scrub(dir string, fix bool) error {
// utility routines // utility routines
func CheckHardLink(srcFileName string, destFileName string) error {
return os.Link(srcFileName, destFileName)
}
func ValidateHardLink(rootDir string) error {
err := ioutil.WriteFile(path.Join(rootDir, "hardlinkcheck.txt"), //nolint: gosec
[]byte("check whether hardlinks work on filesystem"), 0644)
if err != nil {
return err
}
err = CheckHardLink(path.Join(rootDir, "hardlinkcheck.txt"), path.Join(rootDir, "duphardlinkcheck.txt"))
if err != nil {
// Remove hardlinkcheck.txt if hardlink fails
zerr := os.RemoveAll(path.Join(rootDir, "hardlinkcheck.txt"))
if zerr != nil {
return zerr
}
return err
}
err = os.RemoveAll(path.Join(rootDir, "hardlinkcheck.txt"))
if err != nil {
return err
}
return os.RemoveAll(path.Join(rootDir, "duphardlinkcheck.txt"))
}
func dirExists(d string) bool { func dirExists(d string) bool {
fi, err := os.Stat(d) fi, err := os.Stat(d)
if err != nil && os.IsNotExist(err) { if err != nil && os.IsNotExist(err) {

View file

@ -598,3 +598,89 @@ func TestNegativeCases(t *testing.T) {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
} }
func TestHardLink(t *testing.T) {
Convey("Test if filesystem supports hardlink", t, func() {
dir, err := ioutil.TempDir("", "storage-hard-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
err = storage.ValidateHardLink(dir)
So(err, ShouldBeNil)
err = ioutil.WriteFile(path.Join(dir, "hardtest.txt"), []byte("testing hard link code"), 0644) //nolint: gosec
if err != nil {
panic(err)
}
err = os.Chmod(dir, 0400)
if err != nil {
panic(err)
}
err = storage.CheckHardLink(path.Join(dir, "hardtest.txt"), path.Join(dir, "duphardtest.txt"))
So(err, ShouldNotBeNil)
err = os.Chmod(dir, 0644)
if err != nil {
panic(err)
}
})
}
func TestStorageHandler(t *testing.T) {
Convey("Test storage handler", t, func() {
// Create temporary directory
firstRootDir, err := ioutil.TempDir("", "util_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(firstRootDir)
secondRootDir, err := ioutil.TempDir("", "util_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(secondRootDir)
thirdRootDir, err := ioutil.TempDir("", "util_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(thirdRootDir)
log := log.NewLogger("debug", "")
// Create ImageStore
firstStore := storage.NewImageStore(firstRootDir, false, false, log)
secondStore := storage.NewImageStore(secondRootDir, false, false, log)
thirdStore := storage.NewImageStore(thirdRootDir, false, false, log)
storeController := storage.StoreController{}
storeController.DefaultStore = firstStore
subStore := make(map[string]*storage.ImageStore)
subStore["/a"] = secondStore
subStore["/b"] = thirdStore
storeController.SubStore = subStore
is := storeController.GetImageStore("zot-x-test")
So(is.RootDir(), ShouldEqual, firstRootDir)
is = storeController.GetImageStore("a/zot-a-test")
So(is.RootDir(), ShouldEqual, secondRootDir)
is = storeController.GetImageStore("b/zot-b-test")
So(is.RootDir(), ShouldEqual, thirdRootDir)
is = storeController.GetImageStore("c/zot-c-test")
So(is.RootDir(), ShouldEqual, firstRootDir)
})
}