diff --git a/src/api/artifact/abstractor/abstractor_test.go b/src/api/artifact/abstractor/abstractor_test.go index 235a4461a..972a41fb2 100644 --- a/src/api/artifact/abstractor/abstractor_test.go +++ b/src/api/artifact/abstractor/abstractor_test.go @@ -21,8 +21,8 @@ import ( "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/artifact" - htesting "github.com/goharbor/harbor/src/testing" "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob" + repotesting "github.com/goharbor/harbor/src/testing/pkg/repository" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/suite" "testing" @@ -212,7 +212,7 @@ type abstractorTestSuite struct { suite.Suite abstractor Abstractor fetcher *blob.FakeFetcher - repoMgr *htesting.FakeRepositoryManager + repoMgr *repotesting.FakeManager } func (a *abstractorTestSuite) SetupSuite() { @@ -222,7 +222,7 @@ func (a *abstractorTestSuite) SetupSuite() { func (a *abstractorTestSuite) SetupTest() { a.fetcher = &blob.FakeFetcher{} - a.repoMgr = &htesting.FakeRepositoryManager{} + a.repoMgr = &repotesting.FakeManager{} a.abstractor = &abstractor{ repoMgr: a.repoMgr, blobFetcher: a.fetcher, diff --git a/src/api/artifact/abstractor/blob/fetcher.go b/src/api/artifact/abstractor/blob/fetcher.go index b03de9c80..8f3f2d43e 100644 --- a/src/api/artifact/abstractor/blob/fetcher.go +++ b/src/api/artifact/abstractor/blob/fetcher.go @@ -21,7 +21,6 @@ import ( "github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/common/utils/registry/auth" "github.com/goharbor/harbor/src/core/config" - coreutils "github.com/goharbor/harbor/src/core/utils" v1 "github.com/opencontainers/image-spec/specs-go/v1" "io/ioutil" "net/http" @@ -69,7 +68,7 @@ func (f *fetcher) FetchManifest(repository, digest string) (string, []byte, erro // TODO re-implement it based on OCI registry driver func (f *fetcher) FetchLayer(repository, digest string) ([]byte, error) { // TODO read from cache first - client, err := coreutils.NewRepositoryClientForLocal("admin", repository) + client, err := newRepositoryClient(repository) if err != nil { return nil, err } diff --git a/src/api/artifact/abstractor/resolver/chart/chart_test.go b/src/api/artifact/abstractor/resolver/chart/chart_test.go index 6171084b4..9cf1f91a9 100644 --- a/src/api/artifact/abstractor/resolver/chart/chart_test.go +++ b/src/api/artifact/abstractor/resolver/chart/chart_test.go @@ -17,8 +17,8 @@ package chart import ( "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/artifact" - htesting "github.com/goharbor/harbor/src/testing" "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob" + "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/suite" "testing" ) @@ -26,12 +26,12 @@ import ( type resolverTestSuite struct { suite.Suite resolver *resolver - repoMgr *htesting.FakeRepositoryManager + repoMgr *repository.FakeManager blobFetcher *blob.FakeFetcher } func (r *resolverTestSuite) SetupTest() { - r.repoMgr = &htesting.FakeRepositoryManager{} + r.repoMgr = &repository.FakeManager{} r.blobFetcher = &blob.FakeFetcher{} r.resolver = &resolver{ repoMgr: r.repoMgr, diff --git a/src/api/artifact/abstractor/resolver/image/index_test.go b/src/api/artifact/abstractor/resolver/image/index_test.go index aae67dc30..292ff76d9 100644 --- a/src/api/artifact/abstractor/resolver/image/index_test.go +++ b/src/api/artifact/abstractor/resolver/image/index_test.go @@ -16,7 +16,7 @@ package image import ( "github.com/goharbor/harbor/src/pkg/artifact" - htesting "github.com/goharbor/harbor/src/testing" + arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact" "github.com/stretchr/testify/suite" "testing" ) @@ -24,11 +24,11 @@ import ( type indexResolverTestSuite struct { suite.Suite resolver *indexResolver - artMgr *htesting.FakeArtifactManager + artMgr *arttesting.FakeManager } func (i *indexResolverTestSuite) SetupTest() { - i.artMgr = &htesting.FakeArtifactManager{} + i.artMgr = &arttesting.FakeManager{} i.resolver = &indexResolver{ artMgr: i.artMgr, } diff --git a/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go b/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go index 41c48a9b9..f90348392 100644 --- a/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go +++ b/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go @@ -17,8 +17,8 @@ package image import ( "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/artifact" - htesting "github.com/goharbor/harbor/src/testing" "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob" + "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/suite" "testing" ) @@ -26,12 +26,12 @@ import ( type manifestV2ResolverTestSuite struct { suite.Suite resolver *manifestV2Resolver - repoMgr *htesting.FakeRepositoryManager + repoMgr *repository.FakeManager blobFetcher *blob.FakeFetcher } func (m *manifestV2ResolverTestSuite) SetupTest() { - m.repoMgr = &htesting.FakeRepositoryManager{} + m.repoMgr = &repository.FakeManager{} m.blobFetcher = &blob.FakeFetcher{} m.resolver = &manifestV2Resolver{ repoMgr: m.repoMgr, diff --git a/src/api/artifact/controller_test.go b/src/api/artifact/controller_test.go index 8148b51fd..d1a018de6 100644 --- a/src/api/artifact/controller_test.go +++ b/src/api/artifact/controller_test.go @@ -21,7 +21,9 @@ import ( "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/q" "github.com/goharbor/harbor/src/pkg/tag/model/tag" - htesting "github.com/goharbor/harbor/src/testing" + arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact" + repotesting "github.com/goharbor/harbor/src/testing/pkg/repository" + tagtesting "github.com/goharbor/harbor/src/testing/pkg/tag" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "testing" @@ -40,16 +42,16 @@ func (f *fakeAbstractor) Abstract(ctx context.Context, artifact *artifact.Artifa type controllerTestSuite struct { suite.Suite ctl *controller - repoMgr *htesting.FakeRepositoryManager - artMgr *htesting.FakeArtifactManager - tagMgr *htesting.FakeTagManager + repoMgr *repotesting.FakeManager + artMgr *arttesting.FakeManager + tagMgr *tagtesting.FakeManager abstractor *fakeAbstractor } func (c *controllerTestSuite) SetupTest() { - c.repoMgr = &htesting.FakeRepositoryManager{} - c.artMgr = &htesting.FakeArtifactManager{} - c.tagMgr = &htesting.FakeTagManager{} + c.repoMgr = &repotesting.FakeManager{} + c.artMgr = &arttesting.FakeManager{} + c.tagMgr = &tagtesting.FakeManager{} c.abstractor = &fakeAbstractor{} c.ctl = &controller{ repoMgr: c.repoMgr, diff --git a/src/api/repository/controller.go b/src/api/repository/controller.go index 8c8c82a36..abb416002 100644 --- a/src/api/repository/controller.go +++ b/src/api/repository/controller.go @@ -35,8 +35,12 @@ type Controller interface { // The "name" should contain the namespace part. The "created" will be set as true // when the repository is created Ensure(ctx context.Context, name string) (created bool, id int64, err error) + // List repositories according to the query + List(ctx context.Context, query *q.Query) (total int64, repositories []*models.RepoRecord, err error) // Get the repository specified by ID Get(ctx context.Context, id int64) (repository *models.RepoRecord, err error) + // GetByName gets the repository specified by name + GetByName(ctx context.Context, name string) (repository *models.RepoRecord, err error) } // NewController creates an instance of the default repository controller @@ -93,6 +97,14 @@ func (c *controller) Ensure(ctx context.Context, name string) (bool, int64, erro return true, id, nil } +func (c *controller) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) { + return c.repoMgr.List(ctx, query) +} + func (c *controller) Get(ctx context.Context, id int64) (*models.RepoRecord, error) { return c.repoMgr.Get(ctx, id) } + +func (c *controller) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) { + return c.repoMgr.GetByName(ctx, name) +} diff --git a/src/api/repository/controller_test.go b/src/api/repository/controller_test.go index b18aea370..8e7fe8f1f 100644 --- a/src/api/repository/controller_test.go +++ b/src/api/repository/controller_test.go @@ -16,7 +16,8 @@ package repository import ( "github.com/goharbor/harbor/src/common/models" - htesting "github.com/goharbor/harbor/src/testing" + "github.com/goharbor/harbor/src/testing/pkg/project" + "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/suite" "testing" ) @@ -24,13 +25,13 @@ import ( type controllerTestSuite struct { suite.Suite ctl *controller - proMgr *htesting.FakeProjectManager - repoMgr *htesting.FakeRepositoryManager + proMgr *project.FakeManager + repoMgr *repository.FakeManager } func (c *controllerTestSuite) SetupTest() { - c.proMgr = &htesting.FakeProjectManager{} - c.repoMgr = &htesting.FakeRepositoryManager{} + c.proMgr = &project.FakeManager{} + c.repoMgr = &repository.FakeManager{} c.ctl = &controller{ proMgr: c.proMgr, repoMgr: c.repoMgr, @@ -69,6 +70,20 @@ func (c *controllerTestSuite) TestEnsure() { c.Equal(int64(1), id) } +func (c *controllerTestSuite) TestList() { + c.repoMgr.On("List").Return(1, []*models.RepoRecord{ + { + RepositoryID: 1, + }, + }, nil) + total, repositories, err := c.ctl.List(nil, nil) + c.Require().Nil(err) + c.repoMgr.AssertExpectations(c.T()) + c.Equal(int64(1), total) + c.Require().Len(repositories, 1) + c.Equal(int64(1), repositories[0].RepositoryID) +} + func (c *controllerTestSuite) TestGet() { c.repoMgr.On("Get").Return(&models.RepoRecord{ RepositoryID: 1, @@ -79,6 +94,16 @@ func (c *controllerTestSuite) TestGet() { c.Equal(int64(1), repository.RepositoryID) } +func (c *controllerTestSuite) TestGetByName() { + c.repoMgr.On("GetByName").Return(&models.RepoRecord{ + RepositoryID: 1, + }, nil) + repository, err := c.ctl.GetByName(nil, "library/hello-world") + c.Require().Nil(err) + c.repoMgr.AssertExpectations(c.T()) + c.Equal(int64(1), repository.RepositoryID) +} + func TestControllerTestSuite(t *testing.T) { suite.Run(t, &controllerTestSuite{}) } diff --git a/src/core/config/config.go b/src/core/config/config.go index 2b5f945c3..e40d7a7c8 100755 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -217,7 +217,11 @@ func SelfRegistration() (bool, error) { // RegistryURL ... func RegistryURL() (string, error) { - return cfgMgr.Get(common.RegistryURL).GetString(), nil + url := os.Getenv("REGISTRY_URL") + if len(url) == 0 { + url = "http://registry:5000" + } + return url, nil } // InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers diff --git a/src/go.mod b/src/go.mod index f5873c1c7..7a7cf51ce 100644 --- a/src/go.mod +++ b/src/go.mod @@ -76,6 +76,7 @@ require ( github.com/stretchr/testify v1.4.0 github.com/theupdateframework/notary v0.6.1 golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56 + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect diff --git a/src/pkg/retention/controller_test.go b/src/pkg/retention/controller_test.go index 5c28a7d94..761958b68 100644 --- a/src/pkg/retention/controller_test.go +++ b/src/pkg/retention/controller_test.go @@ -1,14 +1,13 @@ package retention import ( - htesting "github.com/goharbor/harbor/src/testing" - "strings" - "testing" - "github.com/goharbor/harbor/src/pkg/retention/dep" "github.com/goharbor/harbor/src/pkg/retention/policy" "github.com/goharbor/harbor/src/pkg/retention/policy/rule" + "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/suite" + "strings" + "testing" ) type ControllerTestSuite struct { @@ -29,7 +28,7 @@ func TestController(t *testing.T) { func (s *ControllerTestSuite) TestPolicy() { projectMgr := &fakeProjectManager{} - repositoryMgr := &htesting.FakeRepositoryManager{} + repositoryMgr := &repository.FakeManager{} retentionScheduler := &fakeRetentionScheduler{} retentionLauncher := &fakeLauncher{} retentionMgr := NewManager() @@ -127,7 +126,7 @@ func (s *ControllerTestSuite) TestPolicy() { func (s *ControllerTestSuite) TestExecution() { projectMgr := &fakeProjectManager{} - repositoryMgr := &htesting.FakeRepositoryManager{} + repositoryMgr := &repository.FakeManager{} retentionScheduler := &fakeRetentionScheduler{} retentionLauncher := &fakeLauncher{} retentionMgr := NewManager() diff --git a/src/pkg/retention/launcher_test.go b/src/pkg/retention/launcher_test.go index 7579129ed..5736689de 100644 --- a/src/pkg/retention/launcher_test.go +++ b/src/pkg/retention/launcher_test.go @@ -16,9 +16,6 @@ package retention import ( "fmt" - htesting "github.com/goharbor/harbor/src/testing" - "testing" - "github.com/goharbor/harbor/src/common/job" "github.com/goharbor/harbor/src/common/models" _ "github.com/goharbor/harbor/src/pkg/art/selectors/doublestar" @@ -27,9 +24,11 @@ import ( "github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/q" hjob "github.com/goharbor/harbor/src/testing/job" + "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "testing" ) type fakeProjectManager struct { @@ -132,7 +131,7 @@ func (f *fakeRetentionManager) ListHistories(executionID int64, query *q.Query) type launchTestSuite struct { suite.Suite projectMgr project.Manager - repositoryMgr *htesting.FakeRepositoryManager + repositoryMgr *repository.FakeManager retentionMgr Manager jobserviceClient job.Client } @@ -150,7 +149,7 @@ func (l *launchTestSuite) SetupTest() { projects: []*models.Project{ pro1, pro2, }} - l.repositoryMgr = &htesting.FakeRepositoryManager{} + l.repositoryMgr = &repository.FakeManager{} l.retentionMgr = &fakeRetentionManager{} l.jobserviceClient = &hjob.MockJobClient{ JobUUID: []string{"1"}, diff --git a/src/pkg/scheduler/hook/handler_test.go b/src/pkg/scheduler/hook/handler_test.go index 99875ae5f..f245aabe6 100644 --- a/src/pkg/scheduler/hook/handler_test.go +++ b/src/pkg/scheduler/hook/handler_test.go @@ -15,16 +15,15 @@ package hook import ( - "testing" - "github.com/goharbor/harbor/src/pkg/scheduler" "github.com/goharbor/harbor/src/pkg/scheduler/model" - htesting "github.com/goharbor/harbor/src/testing" + schedulertesting "github.com/goharbor/harbor/src/testing/pkg/scheduler" "github.com/stretchr/testify/require" + "testing" ) var h = &controller{ - manager: &htesting.FakeSchedulerManager{}, + manager: &schedulertesting.FakeManager{}, } func TestUpdateStatus(t *testing.T) { @@ -33,7 +32,7 @@ func TestUpdateStatus(t *testing.T) { require.NotNil(t, err) // pass - h.manager.(*htesting.FakeSchedulerManager).Schedules = []*model.Schedule{ + h.manager.(*schedulertesting.FakeManager).Schedules = []*model.Schedule{ { ID: 1, Status: "", diff --git a/src/pkg/scheduler/scheduler_test.go b/src/pkg/scheduler/scheduler_test.go index de4bb2a2a..79e6dbbeb 100644 --- a/src/pkg/scheduler/scheduler_test.go +++ b/src/pkg/scheduler/scheduler_test.go @@ -15,13 +15,12 @@ package scheduler import ( - "testing" - - htesting "github.com/goharbor/harbor/src/testing" "github.com/goharbor/harbor/src/testing/job" + schedulertesting "github.com/goharbor/harbor/src/testing/pkg/scheduler" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "testing" ) var sch *scheduler @@ -41,7 +40,7 @@ func (s *schedulerTestSuite) SetupTest() { // recreate the scheduler object sch = &scheduler{ jobserviceClient: &job.MockJobClient{}, - manager: &htesting.FakeSchedulerManager{}, + manager: &schedulertesting.FakeManager{}, } } diff --git a/src/server/registry/blob/blob.go b/src/server/registry/blob/blob.go deleted file mode 100644 index f963c4621..000000000 --- a/src/server/registry/blob/blob.go +++ /dev/null @@ -1,22 +0,0 @@ -package blob - -import ( - "net/http" - "net/http/httputil" -) - -// NewHandler returns the handler to handler catalog request -func NewHandler(proxy *httputil.ReverseProxy) http.Handler { - return &handler{ - proxy: proxy, - } -} - -type handler struct { - proxy *httputil.ReverseProxy -} - -// ServeHTTP ... -func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - h.proxy.ServeHTTP(w, req) -} diff --git a/src/server/registry/blob/blob_test.go b/src/server/registry/blob/blob_test.go deleted file mode 100644 index f3a8cab65..000000000 --- a/src/server/registry/blob/blob_test.go +++ /dev/null @@ -1 +0,0 @@ -package blob diff --git a/src/server/registry/catalog/catalog.go b/src/server/registry/catalog.go similarity index 79% rename from src/server/registry/catalog/catalog.go rename to src/server/registry/catalog.go index 4e4560de2..e27af4269 100644 --- a/src/server/registry/catalog/catalog.go +++ b/src/server/registry/catalog.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package catalog +package registry import ( "encoding/json" "fmt" + "github.com/goharbor/harbor/src/api/repository" ierror "github.com/goharbor/harbor/src/internal/error" - "github.com/goharbor/harbor/src/pkg/repository" reg_error "github.com/goharbor/harbor/src/server/registry/error" "github.com/goharbor/harbor/src/server/registry/util" "net/http" @@ -26,26 +26,17 @@ import ( "strconv" ) -// NewHandler returns the handler to handler catalog request -func NewHandler(repoMgr repository.Manager) http.Handler { - return &handler{ - repoMgr: repoMgr, +func newRepositoryHandler() http.Handler { + return &repositoryHandler{ + repoCtl: repository.Ctl, } } -type handler struct { - repoMgr repository.Manager +type repositoryHandler struct { + repoCtl repository.Controller } -// ServeHTTP ... -func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - switch req.Method { - case http.MethodGet: - h.get(w, req) - } -} - -func (h *handler) get(w http.ResponseWriter, req *http.Request) { +func (r *repositoryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { var maxEntries int var err error @@ -64,13 +55,13 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { var repoNames []string // get all repositories // ToDo filter out the untagged repos - total, repoRecords, err := h.repoMgr.List(req.Context(), nil) + total, repoRecords, err := r.repoCtl.List(req.Context(), nil) if err != nil { reg_error.Handle(w, req, err) return } if total <= 0 { - h.sendResponse(w, req, repoNames) + r.sendResponse(w, req, repoNames) return } for _, r := range repoRecords { @@ -78,7 +69,7 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { } sort.Strings(repoNames) if !withN { - h.sendResponse(w, req, repoNames) + r.sendResponse(w, req, repoNames) return } @@ -109,12 +100,12 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { w.Header().Set("Link", urlStr) } - h.sendResponse(w, req, resRepos) + r.sendResponse(w, req, resRepos) return } // sendResponse ... -func (h *handler) sendResponse(w http.ResponseWriter, req *http.Request, repositoryNames []string) { +func (r *repositoryHandler) sendResponse(w http.ResponseWriter, req *http.Request, repositoryNames []string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") enc := json.NewEncoder(w) if err := enc.Encode(catalogAPIResponse{ diff --git a/src/server/registry/catalog/catalog_test.go b/src/server/registry/catalog/catalog_test.go deleted file mode 100644 index e571e24c6..000000000 --- a/src/server/registry/catalog/catalog_test.go +++ /dev/null @@ -1 +0,0 @@ -package catalog diff --git a/src/server/registry/manifest/manifest_test.go b/src/server/registry/catalog_test.go similarity index 97% rename from src/server/registry/manifest/manifest_test.go rename to src/server/registry/catalog_test.go index b15e65d5f..172fb4596 100644 --- a/src/server/registry/manifest/manifest_test.go +++ b/src/server/registry/catalog_test.go @@ -12,6 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manifest +package registry // TODO diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go deleted file mode 100644 index c5f875e3e..000000000 --- a/src/server/registry/handler.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package registry - -import ( - "github.com/goharbor/harbor/src/server/middleware/contenttrust" - "github.com/goharbor/harbor/src/server/middleware/vulnerable" - - "github.com/goharbor/harbor/src/core/config" - pkg_repo "github.com/goharbor/harbor/src/pkg/repository" - pkg_tag "github.com/goharbor/harbor/src/pkg/tag" - "github.com/goharbor/harbor/src/server/middleware" - "github.com/goharbor/harbor/src/server/middleware/immutable" - "github.com/goharbor/harbor/src/server/middleware/manifestinfo" - "github.com/goharbor/harbor/src/server/middleware/readonly" - "github.com/goharbor/harbor/src/server/middleware/regtoken" - "github.com/goharbor/harbor/src/server/registry/blob" - "github.com/goharbor/harbor/src/server/registry/catalog" - "github.com/goharbor/harbor/src/server/registry/manifest" - "github.com/goharbor/harbor/src/server/registry/tag" - "github.com/gorilla/mux" - "net/http" - "net/http/httputil" - "net/url" -) - -// New return the registry instance to handle the registry APIs -func New(url *url.URL) http.Handler { - // TODO customize the reverse proxy to improve the performance? - proxy := httputil.NewSingleHostReverseProxy(url) - proxy.Director = basicAuthDirector(proxy.Director) - - // create the root rooter - rootRouter := mux.NewRouter() - rootRouter.StrictSlash(true) - - // handle catalog - rootRouter.Path("/v2/_catalog").Methods(http.MethodGet).Handler(catalog.NewHandler(pkg_repo.Mgr)) - - // handle list tag - rootRouter.Path("/v2/{name:.*}/tags/list").Methods(http.MethodGet).Handler(tag.NewHandler(pkg_repo.Mgr, pkg_tag.Mgr)) - - // handle manifest - // TODO maybe we should split it into several sub routers based on the method - manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter() - manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), manifestinfo.Middleware(), regtoken.Middleware(), contenttrust.Middleware(), vulnerable.Middleware())) - manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(proxy)) - manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete())) - - // handle blob - // as we need to apply middleware to the blob requests, so create a sub router to handle the blob APIs - blobRouter := rootRouter.PathPrefix("/v2/{name:.*}/blobs/").Subrouter() - blobRouter.NewRoute().Methods(http.MethodGet).Handler(blob.NewHandler(proxy)) - blobRouter.NewRoute().Methods(http.MethodHead).Handler(blob.NewHandler(proxy)) - blobRouter.NewRoute().Methods(http.MethodPost).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) - blobRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) - blobRouter.NewRoute().Methods(http.MethodPatch).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) - blobRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) - - // all other APIs are proxy to the backend docker registry - rootRouter.PathPrefix("/").Handler(proxy) - - // register middlewares - // TODO add auth middleware - // TODO apply the existing middlewares - // rootRouter.Use(mux.MiddlewareFunc(middleware)) - - return rootRouter -} - -func basicAuthDirector(d func(*http.Request)) func(*http.Request) { - return func(r *http.Request) { - d(r) - if r != nil && !middleware.SkipInjectRegistryCred(r.Context()) { - u, p := config.RegistryCredential() - r.SetBasicAuth(u, p) - } - } -} diff --git a/src/server/registry/manifest/manifest.go b/src/server/registry/manifest.go similarity index 61% rename from src/server/registry/manifest/manifest.go rename to src/server/registry/manifest.go index 935c88e76..cdc63d6aa 100644 --- a/src/server/registry/manifest/manifest.go +++ b/src/server/registry/manifest.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manifest +package registry import ( "github.com/goharbor/harbor/src/api/artifact" @@ -21,45 +21,16 @@ import ( "github.com/goharbor/harbor/src/internal" "github.com/goharbor/harbor/src/server/registry/error" "github.com/goharbor/harbor/src/server/router" - "github.com/gorilla/mux" "github.com/opencontainers/go-digest" "net/http" - "net/http/httputil" "strings" ) -// NewHandler returns the handler to handler manifest requests -func NewHandler(proxy *httputil.ReverseProxy) http.Handler { - return &handler{ - repoCtl: repository.Ctl, - artCtl: artifact.Ctl, - proxy: proxy, - } -} - -type handler struct { - repoCtl repository.Controller - artCtl artifact.Controller - proxy *httputil.ReverseProxy -} - -func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - switch req.Method { - case http.MethodHead, http.MethodGet: - h.get(w, req) - case http.MethodDelete: - h.delete(w, req) - case http.MethodPut: - h.put(w, req) - } -} - // make sure the artifact exist before proxying the request to the backend registry -func (h *handler) get(w http.ResponseWriter, req *http.Request) { - // check the existence in the database first - vars := mux.Vars(req) - reference := vars["reference"] - artifact, err := h.artCtl.GetByReference(req.Context(), vars["name"], reference, nil) +func getManifest(w http.ResponseWriter, req *http.Request) { + repository := router.Param(req.Context(), ":splat") + reference := router.Param(req.Context(), ":reference") + artifact, err := artifact.Ctl.GetByReference(req.Context(), repository, reference, nil) if err != nil { error.Handle(w, req, err) return @@ -69,23 +40,23 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { if _, err = digest.Parse(reference); err != nil { req = req.Clone(req.Context()) req.URL.Path = strings.TrimSuffix(req.URL.Path, reference) + artifact.Digest - req.URL.RawPath = "" req.URL.RawPath = req.URL.EscapedPath() } - h.proxy.ServeHTTP(w, req) + proxy.ServeHTTP(w, req) // TODO fire event(only for GET method), add access log in the event handler } -func (h *handler) delete(w http.ResponseWriter, req *http.Request) { - // just delete the artifact from database - vars := mux.Vars(req) - artifact, err := h.artCtl.GetByReference(req.Context(), vars["name"], vars["reference"], nil) +// just delete the artifact from database +func deleteManifest(w http.ResponseWriter, req *http.Request) { + repository := router.Param(req.Context(), ":splat") + reference := router.Param(req.Context(), ":reference") + art, err := artifact.Ctl.GetByReference(req.Context(), repository, reference, nil) if err != nil { error.Handle(w, req, err) return } - if err = h.artCtl.Delete(req.Context(), artifact.ID); err != nil { + if err = artifact.Ctl.Delete(req.Context(), art.ID); err != nil { error.Handle(w, req, err) return } @@ -94,20 +65,12 @@ func (h *handler) delete(w http.ResponseWriter, req *http.Request) { // TODO fire event, add access log in the event handler } -func (h *handler) put(w http.ResponseWriter, req *http.Request) { - repository, err := router.Param(req.Context(), ":splat") - if err != nil { - error.Handle(w, req, err) - return - } - reference, err := router.Param(req.Context(), ":reference") - if err != nil { - error.Handle(w, req, err) - return - } +func putManifest(w http.ResponseWriter, req *http.Request) { + repo := router.Param(req.Context(), ":splat") + reference := router.Param(req.Context(), ":reference") // make sure the repository exist before pushing the manifest - _, repositoryID, err := h.repoCtl.Ensure(req.Context(), repository) + _, repositoryID, err := repository.Ctl.Ensure(req.Context(), repo) if err != nil { error.Handle(w, req, err) return @@ -115,7 +78,7 @@ func (h *handler) put(w http.ResponseWriter, req *http.Request) { buffer := internal.NewResponseBuffer(w) // proxy the req to the backend docker registry - h.proxy.ServeHTTP(buffer, req) + proxy.ServeHTTP(buffer, req) if !buffer.Success() { if _, err := buffer.Flush(); err != nil { log.Errorf("failed to flush: %v", err) @@ -135,7 +98,7 @@ func (h *handler) put(w http.ResponseWriter, req *http.Request) { tags = append(tags, reference) } - _, _, err = h.artCtl.Ensure(req.Context(), repositoryID, dgt, tags...) + _, _, err = artifact.Ctl.Ensure(req.Context(), repositoryID, dgt, tags...) if err != nil { error.Handle(w, req, err) return diff --git a/src/server/registry/manifest_test.go b/src/server/registry/manifest_test.go new file mode 100644 index 000000000..cc1cf314d --- /dev/null +++ b/src/server/registry/manifest_test.go @@ -0,0 +1,162 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/repository" + ierror "github.com/goharbor/harbor/src/internal/error" + arttesting "github.com/goharbor/harbor/src/testing/api/artifact" + repotesting "github.com/goharbor/harbor/src/testing/api/repository" + "github.com/stretchr/testify/suite" + "net/http" + "net/http/httptest" + "testing" +) + +type manifestTestSuite struct { + suite.Suite + originalRepoCtl repository.Controller + originalArtCtl artifact.Controller + originalProxy http.Handler + repoCtl *repotesting.FakeController + artCtl *arttesting.FakeController +} + +func (m *manifestTestSuite) SetupSuite() { + m.originalRepoCtl = repository.Ctl + m.originalArtCtl = artifact.Ctl + m.originalProxy = proxy +} + +func (m *manifestTestSuite) SetupTest() { + m.repoCtl = &repotesting.FakeController{} + m.artCtl = &arttesting.FakeController{} + repository.Ctl = m.repoCtl + artifact.Ctl = m.artCtl +} + +func (m *manifestTestSuite) TearDownTest() { + proxy = nil +} + +func (m *manifestTestSuite) TearDownSuite() { + repository.Ctl = m.originalRepoCtl + artifact.Ctl = m.originalArtCtl + proxy = m.originalProxy +} + +func (m *manifestTestSuite) TestGetManifest() { + // doesn't exist + req := httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/latest", nil) + w := &httptest.ResponseRecorder{} + + m.artCtl.On("GetByReference").Return(nil, ierror.New(nil).WithCode(ierror.NotFoundCode)) + getManifest(w, req) + m.Equal(http.StatusNotFound, w.Code) + + // reset the mock + m.SetupTest() + + // exist + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet && req.URL.Path == "/v2/library/hello-world/manifests/sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180" { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + // as we cannot set the beego input in the context, here the request doesn't carry reference part + req = httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/", nil) + w = &httptest.ResponseRecorder{} + + art := &artifact.Artifact{} + art.Digest = "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180" + m.artCtl.On("GetByReference").Return(art, nil) + getManifest(w, req) + m.Equal(http.StatusOK, w.Code) +} + +func (m *manifestTestSuite) TestDeleteManifest() { + // doesn't exist + req := httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil) + w := &httptest.ResponseRecorder{} + + m.artCtl.On("GetByReference").Return(nil, ierror.New(nil).WithCode(ierror.NotFoundCode)) + deleteManifest(w, req) + m.Equal(http.StatusNotFound, w.Code) + + // reset the mock + m.SetupTest() + + // reset the mock + m.SetupTest() + + // exist + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Docker-Content-Digest", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180") + return + } + w.WriteHeader(http.StatusOK) + }) + req = httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil) + w = &httptest.ResponseRecorder{} + m.artCtl.On("GetByReference").Return(&artifact.Artifact{}, nil) + m.artCtl.On("Delete").Return(nil) + deleteManifest(w, req) + m.Equal(http.StatusAccepted, w.Code) +} + +func (m *manifestTestSuite) TestPutManifest() { + // the backend registry response with 500 + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNotFound) + }) + req := httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", nil) + w := &httptest.ResponseRecorder{} + m.repoCtl.On("Ensure").Return(false, 1, nil) + putManifest(w, req) + m.Equal(http.StatusInternalServerError, w.Code) + + // reset the mock + m.SetupTest() + + // // the backend registry serves the request successfully + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" { + w.Header().Set("Docker-Content-Digest", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180") + w.WriteHeader(http.StatusCreated) + return + } + w.WriteHeader(http.StatusNotFound) + }) + req = httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", nil) + w = &httptest.ResponseRecorder{} + m.repoCtl.On("Ensure").Return(false, 1, nil) + m.artCtl.On("Ensure").Return(true, 1, nil) + putManifest(w, req) + m.Equal(http.StatusCreated, w.Code) +} + +func TestManifestTestSuite(t *testing.T) { + suite.Run(t, &manifestTestSuite{}) +} diff --git a/src/server/registry/proxy.go b/src/server/registry/proxy.go new file mode 100644 index 000000000..8e68c330c --- /dev/null +++ b/src/server/registry/proxy.go @@ -0,0 +1,47 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/server/middleware" + "net/http" + "net/http/httputil" + "net/url" +) + +var proxy = newProxy() + +func newProxy() http.Handler { + regURL, _ := config.RegistryURL() + url, err := url.Parse(regURL) + if err != nil { + panic(fmt.Sprintf("failed to parse the URL of registry: %v", err)) + } + proxy := httputil.NewSingleHostReverseProxy(url) + proxy.Director = basicAuthDirector(proxy.Director) + return proxy +} + +func basicAuthDirector(d func(*http.Request)) func(*http.Request) { + return func(r *http.Request) { + d(r) + if r != nil && !middleware.SkipInjectRegistryCred(r.Context()) { + u, p := config.RegistryCredential() + r.SetBasicAuth(u, p) + } + } +} diff --git a/src/server/registry/route.go b/src/server/registry/route.go index bec79ee7e..5ea06a1d4 100644 --- a/src/server/registry/route.go +++ b/src/server/registry/route.go @@ -15,39 +15,70 @@ package registry import ( - "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/server/middleware/immutable" - "github.com/goharbor/harbor/src/server/middleware/artifactinfo" + "github.com/goharbor/harbor/src/server/middleware/contenttrust" + "github.com/goharbor/harbor/src/server/middleware/immutable" "github.com/goharbor/harbor/src/server/middleware/manifestinfo" "github.com/goharbor/harbor/src/server/middleware/readonly" + "github.com/goharbor/harbor/src/server/middleware/regtoken" "github.com/goharbor/harbor/src/server/middleware/v2auth" - "github.com/goharbor/harbor/src/server/registry/manifest" + "github.com/goharbor/harbor/src/server/middleware/vulnerable" "github.com/goharbor/harbor/src/server/router" "net/http" - "net/http/httputil" - "net/url" ) // RegisterRoutes for OCI registry APIs func RegisterRoutes() { - // TODO remove - regURL, _ := config.RegistryURL() - url, _ := url.Parse(regURL) - proxy := httputil.NewSingleHostReverseProxy(url) - proxy.Director = basicAuthDirector(proxy.Director) - - router.NewRoute().Path("/v2/*"). + root := router.NewRoute(). + Path("/v2"). Middleware(artifactinfo.Middleware()). - Middleware(v2auth.Middleware()). - Handler(New(url)) - router.NewRoute(). + Middleware(v2auth.Middleware()) + // catalog + root.NewRoute(). + Method(http.MethodGet). + Path("/_catalog"). + Handler(newRepositoryHandler()) + // list tags + root.NewRoute(). + Method(http.MethodGet). + Path("/*/tags/list"). + Handler(newTagHandler()) + // manifest + root.NewRoute(). + Method(http.MethodGet). + Path("/*/manifests/:reference"). + Middleware(manifestinfo.Middleware()). + Middleware(regtoken.Middleware()). + Middleware(contenttrust.Middleware()). + Middleware(vulnerable.Middleware()). + HandlerFunc(getManifest) + root.NewRoute(). + Method(http.MethodHead). + Path("/*/manifests/:reference"). + HandlerFunc(getManifest) + root.NewRoute(). + Method(http.MethodDelete). + Path("/*/manifests/:reference"). + Middleware(readonly.Middleware()). + Middleware(manifestinfo.Middleware()). + Middleware(immutable.MiddlewareDelete()). + HandlerFunc(deleteManifest) + root.NewRoute(). Method(http.MethodPut). - Path("/v2/*/manifests/:reference"). - Middleware(artifactinfo.Middleware()). - Middleware(v2auth.Middleware()). + Path("/*/manifests/:reference"). Middleware(readonly.Middleware()). Middleware(manifestinfo.Middleware()). Middleware(immutable.MiddlewarePush()). - Handler(manifest.NewHandler(proxy)) + HandlerFunc(putManifest) + // blob + root.NewRoute(). + Method(http.MethodPost). + Method(http.MethodPut). + Method(http.MethodPatch). + Method(http.MethodDelete). + Path("/{name:.*}/blobs/"). + Middleware(readonly.Middleware()). + Handler(proxy) + // others + root.NewRoute().Path("/*").Handler(proxy) } diff --git a/src/server/registry/tag/tag.go b/src/server/registry/tag.go similarity index 68% rename from src/server/registry/tag/tag.go rename to src/server/registry/tag.go index 057614130..80744d59a 100644 --- a/src/server/registry/tag/tag.go +++ b/src/server/registry/tag.go @@ -12,46 +12,36 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tag +package registry import ( "encoding/json" "fmt" + "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/repository" ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/q" - "github.com/goharbor/harbor/src/pkg/repository" - "github.com/goharbor/harbor/src/pkg/tag" reg_error "github.com/goharbor/harbor/src/server/registry/error" "github.com/goharbor/harbor/src/server/registry/util" - "github.com/gorilla/mux" + "github.com/goharbor/harbor/src/server/router" "net/http" "sort" "strconv" ) -// NewHandler returns the handler to handle listing tag request -func NewHandler(repoMgr repository.Manager, tagMgr tag.Manager) http.Handler { - return &handler{ - repoMgr: repoMgr, - tagMgr: tagMgr, +func newTagHandler() http.Handler { + return &tagHandler{ + repoCtl: repository.Ctl, + artCtl: artifact.Ctl, } } -type handler struct { - repoMgr repository.Manager - tagMgr tag.Manager - +type tagHandler struct { + repoCtl repository.Controller + artCtl artifact.Controller repositoryName string } -// ServeHTTP ... -func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - switch req.Method { - case http.MethodGet: - h.get(w, req) - } -} - // get return the list of tags // Content-Type: application/json @@ -64,7 +54,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // ... // ] // } -func (h *handler) get(w http.ResponseWriter, req *http.Request) { +func (t *tagHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { var maxEntries int var err error @@ -80,29 +70,26 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { } } - var repoID int64 var tagNames []string - vars := mux.Vars(req) - h.repositoryName = vars["name"] - repoID, err = h.getRepoID(req) + t.repositoryName = router.Param(req.Context(), ":splat") + repository, err := t.repoCtl.GetByName(req.Context(), t.repositoryName) if err != nil { reg_error.Handle(w, req, err) return } // get tags ... - total, tags, err := h.tagMgr.List(req.Context(), &q.Query{ + total, tags, err := t.artCtl.Tags(req.Context(), &q.Query{ Keywords: map[string]interface{}{ - "RepositoryID": repoID, - }, - }) + "RepositoryID": repository.RepositoryID, + }}, nil) if err != nil { reg_error.Handle(w, req, err) return } if total == 0 { - h.sendResponse(w, req, tagNames) + t.sendResponse(w, req, tagNames) return } @@ -111,7 +98,7 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { } sort.Strings(tagNames) if !withN { - h.sendResponse(w, req, tagNames) + t.sendResponse(w, req, tagNames) return } @@ -141,33 +128,16 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) { } w.Header().Set("Link", urlStr) } - h.sendResponse(w, req, resTags) + t.sendResponse(w, req, resTags) return } -// getRepoID ... -func (h *handler) getRepoID(req *http.Request) (int64, error) { - total, repoRecord, err := h.repoMgr.List(req.Context(), &q.Query{ - Keywords: map[string]interface{}{ - "name": h.repositoryName, - }, - }) - if err != nil { - return 0, err - } - if total <= 0 { - err := ierror.New(nil).WithCode(ierror.NotFoundCode).WithMessage("repositoryNotFound") - return 0, err - } - return repoRecord[0].RepositoryID, nil -} - // sendResponse ... -func (h *handler) sendResponse(w http.ResponseWriter, req *http.Request, tagNames []string) { +func (t *tagHandler) sendResponse(w http.ResponseWriter, req *http.Request, tagNames []string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") enc := json.NewEncoder(w) if err := enc.Encode(tagsAPIResponse{ - Name: h.repositoryName, + Name: t.repositoryName, Tags: tagNames, }); err != nil { reg_error.Handle(w, req, err) diff --git a/src/server/registry/tag/tag_test.go b/src/server/registry/tag/tag_test.go deleted file mode 100644 index 686c0dfff..000000000 --- a/src/server/registry/tag/tag_test.go +++ /dev/null @@ -1 +0,0 @@ -package tag diff --git a/src/server/registry/tag_test.go b/src/server/registry/tag_test.go new file mode 100644 index 000000000..172fb4596 --- /dev/null +++ b/src/server/registry/tag_test.go @@ -0,0 +1,17 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +// TODO diff --git a/src/server/router/router.go b/src/server/router/router.go index 4afcdad18..73cd10383 100644 --- a/src/server/router/router.go +++ b/src/server/router/router.go @@ -16,11 +16,11 @@ package router import ( "context" - "errors" "github.com/astaxie/beego" beegocontext "github.com/astaxie/beego/context" "github.com/goharbor/harbor/src/server/middleware" "net/http" + "path/filepath" ) type contextKeyInput struct{} @@ -32,11 +32,19 @@ func NewRoute() *Route { // Route stores the information that matches a request type Route struct { + parent *Route methods []string path string middlewares []middleware.Middleware } +// NewRoute returns a sub route based on the current one +func (r *Route) NewRoute() *Route { + return &Route{ + parent: r, + } +} + // Method sets the method that the route matches func (r *Route) Method(method string) *Route { r.methods = append(r.methods, method) @@ -57,33 +65,49 @@ func (r *Route) Middleware(middleware middleware.Middleware) *Route { // Handler sets the handler that handles the request func (r *Route) Handler(handler http.Handler) { + methods := r.methods + if len(methods) == 0 && r.parent != nil { + methods = r.parent.methods + } + + path := r.path + if r.parent != nil { + path = filepath.Join(r.parent.path, path) + } + + var middlewares []middleware.Middleware + if r.parent != nil { + middlewares = r.parent.middlewares + } + + middlewares = append(middlewares, r.middlewares...) filterFunc := beego.FilterFunc(func(ctx *beegocontext.Context) { ctx.Request = ctx.Request.WithContext( context.WithValue(ctx.Request.Context(), contextKeyInput{}, ctx.Input)) // TODO remove the WithMiddlewares? - middleware.WithMiddlewares(handler, r.middlewares...). + middleware.WithMiddlewares(handler, middlewares...). ServeHTTP(ctx.ResponseWriter, ctx.Request) }) - if len(r.methods) == 0 { + if len(methods) == 0 { beego.Any(r.path, filterFunc) return } - for _, method := range r.methods { + for _, method := range methods { switch method { case http.MethodGet: - beego.Get(r.path, filterFunc) + beego.Get(path, filterFunc) case http.MethodHead: - beego.Head(r.path, filterFunc) + beego.Head(path, filterFunc) case http.MethodPut: - beego.Put(r.path, filterFunc) + beego.Put(path, filterFunc) case http.MethodPatch: - beego.Patch(r.path, filterFunc) + beego.Patch(path, filterFunc) case http.MethodPost: - beego.Post(r.path, filterFunc) + beego.Post(path, filterFunc) case http.MethodDelete: - beego.Delete(r.path, filterFunc) + beego.Delete(path, filterFunc) case http.MethodOptions: - beego.Options(r.path, filterFunc) + beego.Options(path, filterFunc) } } } @@ -93,28 +117,14 @@ func (r *Route) HandlerFunc(f http.HandlerFunc) { r.Handler(f) } -// GetInput returns the input object from the context -func GetInput(context context.Context) (*beegocontext.BeegoInput, error) { - if context == nil { - return nil, errors.New("context is nil") +// Param returns the beego router param by a given key from the context +func Param(ctx context.Context, key string) string { + if ctx == nil { + return "" } - input, ok := context.Value(contextKeyInput{}).(*beegocontext.BeegoInput) + input, ok := ctx.Value(contextKeyInput{}).(*beegocontext.BeegoInput) if !ok { - return nil, errors.New("input not found in the context") + return "" } - return input, nil -} - -// Param returns the router param by a given key from the context -func Param(ctx context.Context, key string) (string, error) { - input, err := GetInput(ctx) - if err != nil { - return "", err - } - return input.Param(key), nil -} - -// Middleware registers the global middleware that executed for all requests that match the path -func Middleware(path string, middleware middleware.Middleware) { - // TODO add middleware function to register global middleware after upgrading to the latest version of beego + return input.Param(key) } diff --git a/src/server/router/router_test.go b/src/server/router/router_test.go index 14964df99..b8382d05d 100644 --- a/src/server/router/router_test.go +++ b/src/server/router/router_test.go @@ -32,6 +32,12 @@ func (r *routerTestSuite) SetupTest() { r.route = NewRoute() } +func (r *routerTestSuite) TestNewRoute() { + sub := r.route.Path("/v2").NewRoute() + r.Require().NotNil(sub.parent) + r.Equal("/v2", sub.parent.path) +} + func (r *routerTestSuite) TestMethod() { r.route.Method(http.MethodGet) r.Equal(http.MethodGet, r.route.methods[0]) @@ -53,27 +59,19 @@ func (r *routerTestSuite) TestMiddleware() { r.Len(r.route.middlewares, 2) } -func (r *routerTestSuite) TestGetInput() { +func (r *routerTestSuite) TestParam() { // nil context - _, err := GetInput(nil) - r.Require().NotNil(err) + value := Param(nil, "key") + r.Empty(value) // context contains wrong type input - _, err = GetInput(context.WithValue(context.Background(), contextKeyInput{}, &Route{})) - r.Require().NotNil(err) + value = Param(context.WithValue(context.Background(), contextKeyInput{}, &Route{}), "key") + r.Empty(value) - // context contains input - input, err := GetInput(context.WithValue(context.Background(), contextKeyInput{}, &beegocontext.BeegoInput{})) - r.Require().Nil(err) - r.Assert().NotNil(input) -} - -func (r *routerTestSuite) TestParam() { + // success input := &beegocontext.BeegoInput{} input.SetParam("key", "value") - value, err := Param(context.WithValue(context.Background(), contextKeyInput{}, input), "key") - r.Require().Nil(err) - r.Assert().NotNil(input) + value = Param(context.WithValue(context.Background(), contextKeyInput{}, input), "key") r.Equal("value", value) } diff --git a/src/testing/api/artifact/controller.go b/src/testing/api/artifact/controller.go new file mode 100644 index 000000000..0f0bac0e4 --- /dev/null +++ b/src/testing/api/artifact/controller.go @@ -0,0 +1,102 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package artifact + +import ( + "context" + "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/pkg/q" + "github.com/stretchr/testify/mock" + "time" +) + +// FakeController is a fake artifact controller that implement src/api/artifact.Controller interface +type FakeController struct { + mock.Mock +} + +// Ensure ... +func (f *FakeController) Ensure(ctx context.Context, repositoryID int64, digest string, tags ...string) (bool, int64, error) { + args := f.Called() + return args.Bool(0), int64(args.Int(1)), args.Error(2) +} + +// List ... +func (f *FakeController) List(ctx context.Context, query *q.Query, option *artifact.Option) (int64, []*artifact.Artifact, error) { + args := f.Called() + var artifacts []*artifact.Artifact + if args.Get(1) != nil { + artifacts = args.Get(1).([]*artifact.Artifact) + } + return int64(args.Int(0)), artifacts, args.Error(2) +} + +// Get ... +func (f *FakeController) Get(ctx context.Context, id int64, option *artifact.Option) (*artifact.Artifact, error) { + args := f.Called() + var art *artifact.Artifact + if args.Get(0) != nil { + art = args.Get(0).(*artifact.Artifact) + } + return art, args.Error(1) +} + +// GetByReference ... +func (f *FakeController) GetByReference(ctx context.Context, repository, reference string, option *artifact.Option) (*artifact.Artifact, error) { + args := f.Called() + var art *artifact.Artifact + if args.Get(0) != nil { + art = args.Get(0).(*artifact.Artifact) + } + return art, args.Error(1) +} + +// Delete ... +func (f *FakeController) Delete(ctx context.Context, id int64) (err error) { + args := f.Called() + return args.Error(0) +} + +// Tags ... +func (f *FakeController) Tags(ctx context.Context, query *q.Query, option *artifact.TagOption) (int64, []*artifact.Tag, error) { + args := f.Called() + var tags []*artifact.Tag + if args.Get(1) != nil { + tags = args.Get(1).([]*artifact.Tag) + } + return int64(args.Int(0)), tags, args.Error(2) +} + +// DeleteTag ... +func (f *FakeController) DeleteTag(ctx context.Context, tagID int64) (err error) { + args := f.Called() + return args.Error(0) +} + +// UpdatePullTime ... +func (f *FakeController) UpdatePullTime(ctx context.Context, artifactID int64, tagID int64, time time.Time) error { + args := f.Called() + return args.Error(0) +} + +// GetSubResource ... +func (f *FakeController) GetSubResource(ctx context.Context, artifactID int64, resource string) (*artifact.Resource, error) { + args := f.Called() + var res *artifact.Resource + if args.Get(0) != nil { + res = args.Get(0).(*artifact.Resource) + } + return res, args.Error(1) +} diff --git a/src/testing/api/repository/controller.go b/src/testing/api/repository/controller.go new file mode 100644 index 000000000..3ab1ecc20 --- /dev/null +++ b/src/testing/api/repository/controller.go @@ -0,0 +1,64 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "context" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/q" + "github.com/stretchr/testify/mock" +) + +// FakeController is a fake repository controller that implement src/api/repository.Controller interface +type FakeController struct { + mock.Mock +} + +// Ensure ... +func (f *FakeController) Ensure(ctx context.Context, name string) (bool, int64, error) { + args := f.Called() + return args.Bool(0), int64(args.Int(1)), args.Error(2) +} + +// List ... +func (f *FakeController) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) { + args := f.Called() + var repositories []*models.RepoRecord + if args.Get(1) != nil { + repositories = args.Get(1).([]*models.RepoRecord) + } + return int64(args.Int(0)), repositories, args.Error(2) + +} + +// Get ... +func (f *FakeController) Get(ctx context.Context, id int64) (*models.RepoRecord, error) { + args := f.Called() + var repository *models.RepoRecord + if args.Get(0) != nil { + repository = args.Get(0).(*models.RepoRecord) + } + return repository, args.Error(1) +} + +// GetByName ... +func (f *FakeController) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) { + args := f.Called() + var repository *models.RepoRecord + if args.Get(0) != nil { + repository = args.Get(0).(*models.RepoRecord) + } + return repository, args.Error(1) +} diff --git a/src/testing/artifact_manager.go b/src/testing/pkg/artifact/manager.go similarity index 67% rename from src/testing/artifact_manager.go rename to src/testing/pkg/artifact/manager.go index aeab562ff..b168651a9 100644 --- a/src/testing/artifact_manager.go +++ b/src/testing/pkg/artifact/manager.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package testing +package artifact import ( "context" @@ -22,13 +22,13 @@ import ( "time" ) -// FakeArtifactManager is a fake artifact manager that implement src/pkg/artifact.Manager interface -type FakeArtifactManager struct { +// FakeManager is a fake artifact manager that implement src/pkg/artifact.Manager interface +type FakeManager struct { mock.Mock } // List ... -func (f *FakeArtifactManager) List(ctx context.Context, query *q.Query) (int64, []*artifact.Artifact, error) { +func (f *FakeManager) List(ctx context.Context, query *q.Query) (int64, []*artifact.Artifact, error) { args := f.Called() var artifacts []*artifact.Artifact if args.Get(1) != nil { @@ -38,7 +38,7 @@ func (f *FakeArtifactManager) List(ctx context.Context, query *q.Query) (int64, } // Get ... -func (f *FakeArtifactManager) Get(ctx context.Context, id int64) (*artifact.Artifact, error) { +func (f *FakeManager) Get(ctx context.Context, id int64) (*artifact.Artifact, error) { args := f.Called() var art *artifact.Artifact if args.Get(0) != nil { @@ -48,19 +48,19 @@ func (f *FakeArtifactManager) Get(ctx context.Context, id int64) (*artifact.Arti } // Create ... -func (f *FakeArtifactManager) Create(ctx context.Context, artifact *artifact.Artifact) (int64, error) { +func (f *FakeManager) Create(ctx context.Context, artifact *artifact.Artifact) (int64, error) { args := f.Called() return int64(args.Int(0)), args.Error(1) } // Delete ... -func (f *FakeArtifactManager) Delete(ctx context.Context, id int64) error { +func (f *FakeManager) Delete(ctx context.Context, id int64) error { args := f.Called() return args.Error(0) } // UpdatePullTime ... -func (f *FakeArtifactManager) UpdatePullTime(ctx context.Context, artifactID int64, time time.Time) error { +func (f *FakeManager) UpdatePullTime(ctx context.Context, artifactID int64, time time.Time) error { args := f.Called() return args.Error(0) } diff --git a/src/testing/project_manager.go b/src/testing/pkg/project/manager.go similarity index 76% rename from src/testing/project_manager.go rename to src/testing/pkg/project/manager.go index 92dd42ab0..90c51a2c9 100644 --- a/src/testing/project_manager.go +++ b/src/testing/pkg/project/manager.go @@ -12,20 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package testing +package project import ( "github.com/goharbor/harbor/src/common/models" "github.com/stretchr/testify/mock" ) -// FakeProjectManager is a fake project manager that implement src/pkg/project.Manager interface -type FakeProjectManager struct { +// FakeManager is a fake project manager that implement src/pkg/project.Manager interface +type FakeManager struct { mock.Mock } // List ... -func (f *FakeProjectManager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) { +func (f *FakeManager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) { args := f.Called() var projects []*models.Project if args.Get(0) != nil { @@ -35,7 +35,7 @@ func (f *FakeProjectManager) List(query ...*models.ProjectQueryParam) ([]*models } // Get ... -func (f *FakeProjectManager) Get(interface{}) (*models.Project, error) { +func (f *FakeManager) Get(interface{}) (*models.Project, error) { args := f.Called() var project *models.Project if args.Get(0) != nil { diff --git a/src/testing/repository_manager.go b/src/testing/pkg/repository/manager.go similarity index 66% rename from src/testing/repository_manager.go rename to src/testing/pkg/repository/manager.go index f6994dd94..5ff176c9f 100644 --- a/src/testing/repository_manager.go +++ b/src/testing/pkg/repository/manager.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package testing +package repository import ( "context" @@ -21,13 +21,13 @@ import ( "github.com/stretchr/testify/mock" ) -// FakeRepositoryManager is a fake repository manager that implement src/pkg/repository.Manager interface -type FakeRepositoryManager struct { +// FakeManager is a fake repository manager that implement src/pkg/repository.Manager interface +type FakeManager struct { mock.Mock } // List ... -func (f *FakeRepositoryManager) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) { +func (f *FakeManager) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) { args := f.Called() var repositories []*models.RepoRecord if args.Get(1) != nil { @@ -37,7 +37,7 @@ func (f *FakeRepositoryManager) List(ctx context.Context, query *q.Query) (int64 } // Get ... -func (f *FakeRepositoryManager) Get(ctx context.Context, id int64) (*models.RepoRecord, error) { +func (f *FakeManager) Get(ctx context.Context, id int64) (*models.RepoRecord, error) { args := f.Called() var repository *models.RepoRecord if args.Get(0) != nil { @@ -47,7 +47,7 @@ func (f *FakeRepositoryManager) Get(ctx context.Context, id int64) (*models.Repo } // GetByName ... -func (f *FakeRepositoryManager) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) { +func (f *FakeManager) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) { args := f.Called() var repository *models.RepoRecord if args.Get(0) != nil { @@ -57,19 +57,19 @@ func (f *FakeRepositoryManager) GetByName(ctx context.Context, name string) (*mo } // Delete ... -func (f *FakeRepositoryManager) Delete(ctx context.Context, id int64) error { +func (f *FakeManager) Delete(ctx context.Context, id int64) error { args := f.Called() return args.Error(0) } // Create ... -func (f *FakeRepositoryManager) Create(ctx context.Context, repository *models.RepoRecord) (int64, error) { +func (f *FakeManager) Create(ctx context.Context, repository *models.RepoRecord) (int64, error) { args := f.Called() return int64(args.Int(0)), args.Error(1) } // Update ... -func (f *FakeRepositoryManager) Update(ctx context.Context, repository *models.RepoRecord, props ...string) error { +func (f *FakeManager) Update(ctx context.Context, repository *models.RepoRecord, props ...string) error { args := f.Called() return args.Error(0) } diff --git a/src/testing/scheduler.go b/src/testing/pkg/scheduler/scheduler.go similarity index 76% rename from src/testing/scheduler.go rename to src/testing/pkg/scheduler/scheduler.go index c7e502e8e..5675d8baf 100644 --- a/src/testing/scheduler.go +++ b/src/testing/pkg/scheduler/scheduler.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package testing +package scheduler import ( "fmt" @@ -20,14 +20,14 @@ import ( "github.com/goharbor/harbor/src/pkg/scheduler/model" ) -// FakeSchedulerManager ... -type FakeSchedulerManager struct { +// FakeManager ... +type FakeManager struct { idCounter int64 Schedules []*model.Schedule } // Create ... -func (f *FakeSchedulerManager) Create(schedule *model.Schedule) (int64, error) { +func (f *FakeManager) Create(schedule *model.Schedule) (int64, error) { f.idCounter++ id := f.idCounter schedule.ID = id @@ -36,7 +36,7 @@ func (f *FakeSchedulerManager) Create(schedule *model.Schedule) (int64, error) { } // Update ... -func (f *FakeSchedulerManager) Update(schedule *model.Schedule, props ...string) error { +func (f *FakeManager) Update(schedule *model.Schedule, props ...string) error { for i, sch := range f.Schedules { if sch.ID == schedule.ID { f.Schedules[i] = schedule @@ -47,7 +47,7 @@ func (f *FakeSchedulerManager) Update(schedule *model.Schedule, props ...string) } // Delete ... -func (f *FakeSchedulerManager) Delete(id int64) error { +func (f *FakeManager) Delete(id int64) error { length := len(f.Schedules) for i, sch := range f.Schedules { if sch.ID == id { @@ -62,7 +62,7 @@ func (f *FakeSchedulerManager) Delete(id int64) error { } // Get ... -func (f *FakeSchedulerManager) Get(id int64) (*model.Schedule, error) { +func (f *FakeManager) Get(id int64) (*model.Schedule, error) { for _, sch := range f.Schedules { if sch.ID == id { return sch, nil @@ -72,6 +72,6 @@ func (f *FakeSchedulerManager) Get(id int64) (*model.Schedule, error) { } // List ... -func (f *FakeSchedulerManager) List(...*model.ScheduleQuery) ([]*model.Schedule, error) { +func (f *FakeManager) List(...*model.ScheduleQuery) ([]*model.Schedule, error) { return f.Schedules, nil } diff --git a/src/testing/tag_manager.go b/src/testing/pkg/tag/manager.go similarity index 69% rename from src/testing/tag_manager.go rename to src/testing/pkg/tag/manager.go index 7c664eb86..133704390 100644 --- a/src/testing/tag_manager.go +++ b/src/testing/pkg/tag/manager.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package testing +package tag import ( "context" @@ -21,13 +21,13 @@ import ( "github.com/stretchr/testify/mock" ) -// FakeTagManager is a fake tag manager that implement the src/pkg/tag.Manager interface -type FakeTagManager struct { +// FakeManager is a fake tag manager that implement the src/pkg/tag.Manager interface +type FakeManager struct { mock.Mock } // List ... -func (f *FakeTagManager) List(ctx context.Context, query *q.Query) (int64, []*tag.Tag, error) { +func (f *FakeManager) List(ctx context.Context, query *q.Query) (int64, []*tag.Tag, error) { args := f.Called() var tags []*tag.Tag if args.Get(1) != nil { @@ -37,7 +37,7 @@ func (f *FakeTagManager) List(ctx context.Context, query *q.Query) (int64, []*ta } // Get ... -func (f *FakeTagManager) Get(ctx context.Context, id int64) (*tag.Tag, error) { +func (f *FakeManager) Get(ctx context.Context, id int64) (*tag.Tag, error) { args := f.Called() var tg *tag.Tag if args.Get(0) != nil { @@ -47,19 +47,19 @@ func (f *FakeTagManager) Get(ctx context.Context, id int64) (*tag.Tag, error) { } // Create ... -func (f *FakeTagManager) Create(ctx context.Context, tag *tag.Tag) (int64, error) { +func (f *FakeManager) Create(ctx context.Context, tag *tag.Tag) (int64, error) { args := f.Called() return int64(args.Int(0)), args.Error(1) } // Update ... -func (f *FakeTagManager) Update(ctx context.Context, tag *tag.Tag, props ...string) error { +func (f *FakeManager) Update(ctx context.Context, tag *tag.Tag, props ...string) error { args := f.Called() return args.Error(0) } // Delete ... -func (f *FakeTagManager) Delete(ctx context.Context, id int64) error { +func (f *FakeManager) Delete(ctx context.Context, id int64) error { args := f.Called() return args.Error(0) } diff --git a/tests/robot-cases/Group0-BAT/API_DB.robot b/tests/robot-cases/Group0-BAT/API_DB.robot index 67c47e3ab..f17cb70f0 100644 --- a/tests/robot-cases/Group0-BAT/API_DB.robot +++ b/tests/robot-cases/Group0-BAT/API_DB.robot @@ -36,9 +36,8 @@ Test Case - Edit Project Creation # Harbor API Test ./tests/apitests/python/test_scan_image.py Test Case - Manage Project Member Harbor API Test ./tests/apitests/python/test_manage_project_member.py -# TODO uncomment this after enable content trust middleware -# Test Case - Project Level Policy Content Trust -# Harbor API Test ./tests/apitests/python/test_project_level_policy_content_trust.py +Test Case - Project Level Policy Content Trust + Harbor API Test ./tests/apitests/python/test_project_level_policy_content_trust.py # TODO uncomment this after we move the accesslog away from registry notificaiton # TODO potentially #10602 may also fix this. # Test Case - User View Logs diff --git a/tests/travis/ut_run.sh b/tests/travis/ut_run.sh index 21b68dd81..f835b3e63 100755 --- a/tests/travis/ut_run.sh +++ b/tests/travis/ut_run.sh @@ -4,7 +4,7 @@ set -x set -e export POSTGRESQL_HOST=$1 -export REGISTRY_URL=$1:5000 +export REGISTRY_URL=http://$1:5000 export CHROME_BIN=chromium-browser #export DISPLAY=:99.0 #sh -e /etc/init.d/xvfb start