From cbfd342333ffa4b2fe76b7e8948235efd3535fac Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Wed, 2 Oct 2019 11:29:57 -0700 Subject: [PATCH] endpoint to paginate through all repos --- CHANGELOG.md | 1 + core/repo.go | 4 ++ handler/api/api.go | 97 +++++++++++++++++++++------------------- handler/api/repos/all.go | 55 +++++++++++++++++++++++ mock/mock_gen.go | 15 +++++++ store/repos/repos.go | 26 +++++++++++ 6 files changed, 152 insertions(+), 46 deletions(-) create mode 100644 handler/api/repos/all.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b8015d1d..bb30c54e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - added nsswitch to docker images - option to auto-cancel pending builds when newer build enqueued, by [@bradrydzewski](https://github.com/bradrydzewski). [#1980](https://github.com/drone/drone/issues/1980). +- endpoint to list all repositories in the database, by [@bradrydzewski](https://github.com/bradrydzewski). [#2785](https://github.com/drone/drone/issues/2785). ## [1.5.1] - 2019-09-30 ### Added diff --git a/core/repo.go b/core/repo.go index cb2b5048..1a5c3400 100644 --- a/core/repo.go +++ b/core/repo.go @@ -82,6 +82,10 @@ type ( // the datastore with incomplete builds. ListIncomplete(context.Context) ([]*Repository, error) + // ListAll returns a paginated list of all repositories + // stored in the database, including disabled repositories. + ListAll(ctx context.Context, limit, offset int) ([]*Repository, error) + // Find returns a repository from the datastore. Find(context.Context, int64) (*Repository, error) diff --git a/handler/api/api.go b/handler/api/api.go index 1ceabd70..6df801a0 100644 --- a/handler/api/api.go +++ b/handler/api/api.go @@ -154,67 +154,72 @@ func (s Server) Handler() http.Handler { cors := cors.New(corsOpts) r.Use(cors.Handler) - r.Route("/repos/{owner}/{name}", func(r chi.Router) { - r.Use(acl.InjectRepository(s.Repoz, s.Repos, s.Perms)) - r.Use(acl.CheckReadAccess()) - - r.Get("/", repos.HandleFind()) + r.Route("/repos", func(r chi.Router) { r.With( acl.CheckAdminAccess(), - ).Patch("/", repos.HandleUpdate(s.Repos)) - r.With( - acl.CheckAdminAccess(), - ).Post("/", repos.HandleEnable(s.Hooks, s.Repos, s.Webhook)) - r.With( - acl.CheckAdminAccess(), - ).Delete("/", repos.HandleDisable(s.Repos, s.Webhook)) - r.With( - acl.CheckAdminAccess(), - ).Post("/chown", repos.HandleChown(s.Repos)) - r.With( - acl.CheckAdminAccess(), - ).Post("/repair", repos.HandleRepair(s.Hooks, s.Repoz, s.Repos, s.Users, s.System.Link)) + ).Get("/", repos.HandleAll(s.Repos)) - r.Route("/builds", func(r chi.Router) { - r.Get("/", builds.HandleList(s.Repos, s.Builds)) - r.With(acl.CheckWriteAccess()).Post("/", builds.HandleCreate(s.Repos, s.Commits, s.Triggerer)) - - r.Get("/latest", builds.HandleLast(s.Repos, s.Builds, s.Stages)) - r.Get("/{number}", builds.HandleFind(s.Repos, s.Builds, s.Stages)) - r.Get("/{number}/logs/{stage}/{step}", logs.HandleFind(s.Repos, s.Builds, s.Stages, s.Steps, s.Logs)) - - r.With( - acl.CheckWriteAccess(), - ).Post("/{number}", builds.HandleRetry(s.Repos, s.Builds, s.Triggerer)) - - r.With( - acl.CheckWriteAccess(), - ).Delete("/{number}", builds.HandleCancel(s.Users, s.Repos, s.Builds, s.Stages, s.Steps, s.Status, s.Scheduler, s.Webhook)) - - r.With( - acl.CheckWriteAccess(), - ).Post("/{number}/promote", builds.HandlePromote(s.Repos, s.Builds, s.Triggerer)) + r.Route("/{owner}/{name}", func(r chi.Router) { + r.Use(acl.InjectRepository(s.Repoz, s.Repos, s.Perms)) + r.Use(acl.CheckReadAccess()) + r.Get("/", repos.HandleFind()) r.With( acl.CheckAdminAccess(), - ).Post("/{number}/rollback", builds.HandleRollback(s.Repos, s.Builds, s.Triggerer)) - + ).Patch("/", repos.HandleUpdate(s.Repos)) r.With( acl.CheckAdminAccess(), - ).Post("/{number}/decline/{stage}", stages.HandleDecline(s.Repos, s.Builds, s.Stages)) - + ).Post("/", repos.HandleEnable(s.Hooks, s.Repos, s.Webhook)) r.With( acl.CheckAdminAccess(), - ).Post("/{number}/approve/{stage}", stages.HandleApprove(s.Repos, s.Builds, s.Stages, s.Scheduler)) - + ).Delete("/", repos.HandleDisable(s.Repos, s.Webhook)) r.With( acl.CheckAdminAccess(), - ).Delete("/{number}/logs/{stage}/{step}", logs.HandleDelete(s.Repos, s.Builds, s.Stages, s.Steps, s.Logs)) - + ).Post("/chown", repos.HandleChown(s.Repos)) r.With( acl.CheckAdminAccess(), - ).Delete("/", builds.HandlePurge(s.Repos, s.Builds)) + ).Post("/repair", repos.HandleRepair(s.Hooks, s.Repoz, s.Repos, s.Users, s.System.Link)) + r.Route("/builds", func(r chi.Router) { + r.Get("/", builds.HandleList(s.Repos, s.Builds)) + r.With(acl.CheckWriteAccess()).Post("/", builds.HandleCreate(s.Repos, s.Commits, s.Triggerer)) + + r.Get("/latest", builds.HandleLast(s.Repos, s.Builds, s.Stages)) + r.Get("/{number}", builds.HandleFind(s.Repos, s.Builds, s.Stages)) + r.Get("/{number}/logs/{stage}/{step}", logs.HandleFind(s.Repos, s.Builds, s.Stages, s.Steps, s.Logs)) + + r.With( + acl.CheckWriteAccess(), + ).Post("/{number}", builds.HandleRetry(s.Repos, s.Builds, s.Triggerer)) + + r.With( + acl.CheckWriteAccess(), + ).Delete("/{number}", builds.HandleCancel(s.Users, s.Repos, s.Builds, s.Stages, s.Steps, s.Status, s.Scheduler, s.Webhook)) + + r.With( + acl.CheckWriteAccess(), + ).Post("/{number}/promote", builds.HandlePromote(s.Repos, s.Builds, s.Triggerer)) + + r.With( + acl.CheckAdminAccess(), + ).Post("/{number}/rollback", builds.HandleRollback(s.Repos, s.Builds, s.Triggerer)) + + r.With( + acl.CheckAdminAccess(), + ).Post("/{number}/decline/{stage}", stages.HandleDecline(s.Repos, s.Builds, s.Stages)) + + r.With( + acl.CheckAdminAccess(), + ).Post("/{number}/approve/{stage}", stages.HandleApprove(s.Repos, s.Builds, s.Stages, s.Scheduler)) + + r.With( + acl.CheckAdminAccess(), + ).Delete("/{number}/logs/{stage}/{step}", logs.HandleDelete(s.Repos, s.Builds, s.Stages, s.Steps, s.Logs)) + + r.With( + acl.CheckAdminAccess(), + ).Delete("/", builds.HandlePurge(s.Repos, s.Builds)) + }) }) r.Route("/secrets", func(r chi.Router) { diff --git a/handler/api/repos/all.go b/handler/api/repos/all.go new file mode 100644 index 00000000..fa7f3ae7 --- /dev/null +++ b/handler/api/repos/all.go @@ -0,0 +1,55 @@ +// Copyright 2019 Drone IO, Inc. +// +// 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 repos + +import ( + "net/http" + "strconv" + + "github.com/drone/drone/core" + "github.com/drone/drone/handler/api/render" + "github.com/drone/drone/logger" +) + +// HandleAll returns an http.HandlerFunc that processes http +// requests to list all repositories in the database. +func HandleAll(repos core.RepositoryStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + page = r.FormValue("page") + perPage = r.FormValue("per_page") + ) + offset, _ := strconv.Atoi(page) + limit, _ := strconv.Atoi(perPage) + if limit < 1 { // || limit > 100 + limit = 25 + } + switch offset { + case 0, 1: + offset = 0 + default: + offset = (offset - 1) * limit + } + repo, err := repos.ListAll(r.Context(), limit, offset) + if err != nil { + render.InternalError(w, err) + logger.FromRequest(r). + WithError(err). + Debugln("api: cannot list repositories") + } else { + render.JSON(w, repo, 200) + } + } +} diff --git a/mock/mock_gen.go b/mock/mock_gen.go index 8bfeaa71..53ac2fd4 100644 --- a/mock/mock_gen.go +++ b/mock/mock_gen.go @@ -1768,6 +1768,21 @@ func (mr *MockRepositoryStoreMockRecorder) List(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRepositoryStore)(nil).List), arg0, arg1) } +// ListAll mocks base method +func (m *MockRepositoryStore) ListAll(arg0 context.Context, arg1, arg2 int) ([]*core.Repository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAll", arg0, arg1, arg2) + ret0, _ := ret[0].([]*core.Repository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAll indicates an expected call of ListAll +func (mr *MockRepositoryStoreMockRecorder) ListAll(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockRepositoryStore)(nil).ListAll), arg0, arg1, arg2) +} + // ListIncomplete mocks base method func (m *MockRepositoryStore) ListIncomplete(arg0 context.Context) ([]*core.Repository, error) { m.ctrl.T.Helper() diff --git a/store/repos/repos.go b/store/repos/repos.go index c30b32f4..12413299 100644 --- a/store/repos/repos.go +++ b/store/repos/repos.go @@ -104,6 +104,27 @@ func (s *repoStore) ListIncomplete(ctx context.Context) ([]*core.Repository, err return out, err } +func (s *repoStore) ListAll(ctx context.Context, limit, offset int) ([]*core.Repository, error) { + var out []*core.Repository + err := s.db.View(func(queryer db.Queryer, binder db.Binder) error { + params := map[string]interface{}{ + "limit": limit, + "offset": offset, + } + query, args, err := binder.BindNamed(queryAll, params) + if err != nil { + return err + } + rows, err := queryer.Query(query, args...) + if err != nil { + return err + } + out, err = scanRows(rows) + return err + }) + return out, err +} + func (s *repoStore) Find(ctx context.Context, id int64) (*core.Repository, error) { out := &core.Repository{ID: id} err := s.db.View(func(queryer db.Queryer, binder db.Binder) error { @@ -329,6 +350,11 @@ WHERE perms.perm_user_id = :user_id ORDER BY repo_slug ASC ` +const queryAll = queryCols + ` +FROM repos +LIMIT :limit OFFSET :offset +` + const stmtDelete = ` DELETE FROM repos WHERE repo_id = :repo_id `