diff --git a/cmd/hub/handlers/handlers.go b/cmd/hub/handlers/handlers.go index 11f5cf6a..05d8268e 100644 --- a/cmd/hub/handlers/handlers.go +++ b/cmd/hub/handlers/handlers.go @@ -172,6 +172,7 @@ func (h *Handlers) setupRouter() { r.Route("/repositories", func(r chi.Router) { r.Use(h.Users.RequireLogin) r.Get("/", h.Repositories.GetAll) + r.Get("/{kind:^helm$|^falco$|^olm$|^opa$}", h.Repositories.GetByKind) r.Route("/user", func(r chi.Router) { r.Get("/", h.Repositories.GetOwnedByUser) r.Post("/", h.Repositories.Add) diff --git a/cmd/hub/handlers/repo/handlers.go b/cmd/hub/handlers/repo/handlers.go index 3cf96d6b..dd8d779d 100644 --- a/cmd/hub/handlers/repo/handlers.go +++ b/cmd/hub/handlers/repo/handlers.go @@ -92,11 +92,29 @@ func (h *Handlers) Delete(w http.ResponseWriter, r *http.Request) { func (h *Handlers) GetAll(w http.ResponseWriter, r *http.Request) { dataJSON, err := h.repoManager.GetAllJSON(r.Context()) if err != nil { - h.logger.Error().Err(err).Str("method", "GetAllJSON").Send() + h.logger.Error().Err(err).Str("method", "GetAll").Send() helpers.RenderErrorJSON(w, err) return } - helpers.RenderJSON(w, dataJSON, 0, http.StatusOK) + helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK) +} + +// GetByKind is an http handler that returns all the repositories available of +// the kind provided. +func (h *Handlers) GetByKind(w http.ResponseWriter, r *http.Request) { + kind, err := hub.GetKindFromName(chi.URLParam(r, "kind")) + if err != nil { + h.logger.Error().Err(err).Str("method", "GetByKind").Msg("invalid kind") + helpers.RenderErrorJSON(w, hub.ErrInvalidInput) + return + } + dataJSON, err := h.repoManager.GetByKindJSON(r.Context(), kind) + if err != nil { + h.logger.Error().Err(err).Str("method", "GetByKind").Send() + helpers.RenderErrorJSON(w, err) + return + } + helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK) } // GetOwnedByOrg is an http handler that returns the repositories owned by the diff --git a/cmd/hub/handlers/repo/handlers_test.go b/cmd/hub/handlers/repo/handlers_test.go index 0c7e9622..e1c49e14 100644 --- a/cmd/hub/handlers/repo/handlers_test.go +++ b/cmd/hub/handlers/repo/handlers_test.go @@ -370,7 +370,7 @@ func TestGetAll(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, "application/json", h.Get("Content-Type")) - assert.Equal(t, helpers.BuildCacheControlHeader(0), h.Get("Cache-Control")) + assert.Equal(t, helpers.BuildCacheControlHeader(helpers.DefaultAPICacheMaxAge), h.Get("Cache-Control")) assert.Equal(t, []byte("dataJSON"), data) hw.rm.AssertExpectations(t) }) @@ -391,6 +391,66 @@ func TestGetAll(t *testing.T) { }) } +func TestGetByKind(t *testing.T) { + rctx := &chi.Context{ + URLParams: chi.RouteParams{ + Keys: []string{"kind"}, + Values: []string{"olm"}, + }, + } + + t.Run("invalid kind provided", func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", nil) + r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID")) + + hw := newHandlersWrapper() + hw.h.GetByKind(w, r) + resp := w.Result() + defer resp.Body.Close() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + hw.rm.AssertExpectations(t) + }) + + t.Run("get repositories by kind succeeded", func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", nil) + r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID")) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx)) + + hw := newHandlersWrapper() + hw.rm.On("GetByKindJSON", r.Context(), hub.OLM).Return([]byte("dataJSON"), nil) + hw.h.GetByKind(w, r) + resp := w.Result() + defer resp.Body.Close() + h := resp.Header + data, _ := ioutil.ReadAll(resp.Body) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", h.Get("Content-Type")) + assert.Equal(t, helpers.BuildCacheControlHeader(helpers.DefaultAPICacheMaxAge), h.Get("Cache-Control")) + assert.Equal(t, []byte("dataJSON"), data) + hw.rm.AssertExpectations(t) + }) + + t.Run("error getting repositories by kind", func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", nil) + r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID")) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx)) + + hw := newHandlersWrapper() + hw.rm.On("GetByKindJSON", r.Context(), hub.OLM).Return(nil, tests.ErrFakeDatabaseFailure) + hw.h.GetByKind(w, r) + resp := w.Result() + defer resp.Body.Close() + + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + hw.rm.AssertExpectations(t) + }) +} + func TestGetOwnedByOrg(t *testing.T) { rctx := &chi.Context{ URLParams: chi.RouteParams{ diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index a2a26edf..28c142a1 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -381,31 +381,39 @@ paths: schema: type: array items: - type: object - properties: - kind: - $ref: "#/components/schemas/RepositoryKind" - name: - type: string - example: repo1 - display_name: - type: string - example: Repo 1 - user_alias: - type: string - example: jdoe - organization_name: - type: string - example: org1 - organization_display_name: - type: string - example: Organization 1 + $ref: "#/components/schemas/RepositoryData" "401": $ref: "#/components/responses/UnauthorizedError" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalServerError" + /repositories/{^helm$|^falco$|^opa$|^olm$}: + get: + tags: + - Repositories + security: + - ApiKeyAuth: [] + - CookieAuth: [] + summary: Get all available repositories of the provided kind + parameters: + - $ref: "#/components/parameters/RepoKindParam" + responses: + "200": + description: "" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RepositoryData" + "401": + $ref: "#/components/responses/UnauthorizedError" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalServerError" + /repositories/user: get: tags: @@ -1975,38 +1983,7 @@ components: created_at: type: integer repository: - type: object - properties: - repository_id: - type: string - format: uuid - url: - type: string - nullable: false - format: uri - example: "http://repo-url" - kind: - $ref: "#/components/schemas/RepositoryKind" - name: - type: string - nullable: false - example: repo1 - display_name: - type: string - example: Repository 1 - user_alias: - type: string - example: jdoe - organization_name: - type: string - example: org1 - organization_display_name: - type: string - example: Organization 1 - verified_publisher: - type: boolean - official: - type: boolean + $ref: "#/components/schemas/RepositoryData" nullable: false Repository: allOf: @@ -2035,6 +2012,19 @@ components: * `1` - Falco rules * `2` - OPA policies * `3` - OLM operators + RepositoryKindParam: + type: string + enum: + - helm + - opa + - falco + - olm + description: | + Repository kind name: + * `helm` - Helm charts + * `falco` - Falco rules + * `opa` - OPA policies + * `olm` - OLM operators RepositorySummary: type: object properties: @@ -2057,6 +2047,39 @@ components: required: - name - url + RepositoryData: + type: object + properties: + repository_id: + type: string + format: uuid + url: + type: string + nullable: false + format: uri + example: "http://repo-url" + kind: + $ref: "#/components/schemas/RepositoryKind" + name: + type: string + nullable: false + example: repo1 + display_name: + type: string + example: Repository 1 + user_alias: + type: string + example: jdoe + organization_name: + type: string + example: org1 + organization_display_name: + type: string + example: Organization 1 + verified_publisher: + type: boolean + official: + type: boolean Organization: allOf: - $ref: "#/components/schemas/OrganizationSummary" @@ -2375,6 +2398,13 @@ components: example: pkg1 required: true description: Package name + RepoKindParam: + in: path + name: ^helm$|^falco$|^opa$|^olm$ + schema: + $ref: "#/components/schemas/RepositoryKindParam" + required: true + description: Package kind name RepoNameParam: in: path name: repoName diff --git a/internal/hub/repo.go b/internal/hub/repo.go index 92507ccb..10276181 100644 --- a/internal/hub/repo.go +++ b/internal/hub/repo.go @@ -103,6 +103,7 @@ type RepositoryManager interface { GetAllJSON(ctx context.Context) ([]byte, error) GetByID(ctx context.Context, repositorID string) (*Repository, error) GetByKind(ctx context.Context, kind RepositoryKind) ([]*Repository, error) + GetByKindJSON(ctx context.Context, kind RepositoryKind) ([]byte, error) GetByName(ctx context.Context, name string) (*Repository, error) GetMetadata(mdFile string) (*RepositoryMetadata, error) GetPackagesDigest(ctx context.Context, repositoryID string) (map[string]string, error) diff --git a/internal/repo/manager.go b/internal/repo/manager.go index 6df795c6..373026be 100644 --- a/internal/repo/manager.go +++ b/internal/repo/manager.go @@ -256,6 +256,12 @@ func (m *Manager) GetByKind(ctx context.Context, kind hub.RepositoryKind) ([]*hu return r, err } +// GetByKindJSON returns all available repositories of the provided kind as a +// json array, which is built by the database. +func (m *Manager) GetByKindJSON(ctx context.Context, kind hub.RepositoryKind) ([]byte, error) { + return m.dbQueryJSON(ctx, "select get_repositories_by_kind($1::int)", kind) +} + // GetByName returns the repository identified by the name provided. func (m *Manager) GetByName(ctx context.Context, name string) (*hub.Repository, error) { // Validate input diff --git a/internal/repo/manager_test.go b/internal/repo/manager_test.go index 5bdebabe..289b351c 100644 --- a/internal/repo/manager_test.go +++ b/internal/repo/manager_test.go @@ -719,6 +719,33 @@ func TestGetByKind(t *testing.T) { db.AssertExpectations(t) } +func TestGetByKindJSON(t *testing.T) { + dbQuery := "select get_repositories_by_kind($1::int)" + ctx := context.WithValue(context.Background(), hub.UserIDKey, "userID") + + t.Run("database error", func(t *testing.T) { + db := &tests.DBMock{} + db.On("QueryRow", ctx, dbQuery, hub.OLM).Return(nil, tests.ErrFakeDatabaseFailure) + m := NewManager(cfg, db) + + dataJSON, err := m.GetByKindJSON(ctx, hub.OLM) + assert.Equal(t, tests.ErrFakeDatabaseFailure, err) + assert.Nil(t, dataJSON) + db.AssertExpectations(t) + }) + + t.Run("all repositories data returned successfully", func(t *testing.T) { + db := &tests.DBMock{} + db.On("QueryRow", ctx, dbQuery, hub.OLM).Return([]byte("dataJSON"), nil) + m := NewManager(cfg, db) + + dataJSON, err := m.GetByKindJSON(ctx, hub.OLM) + assert.NoError(t, err) + assert.Equal(t, []byte("dataJSON"), dataJSON) + db.AssertExpectations(t) + }) +} + func TestGetByName(t *testing.T) { dbQuery := "select get_repository_by_name($1::text)" ctx := context.Background() diff --git a/internal/repo/mock.go b/internal/repo/mock.go index 5ec4b2be..0eccae75 100644 --- a/internal/repo/mock.go +++ b/internal/repo/mock.go @@ -65,6 +65,13 @@ func (m *ManagerMock) GetByKind(ctx context.Context, kind hub.RepositoryKind) ([ return data, args.Error(1) } +// GetByKindJSON implements the RepositoryManager interface. +func (m *ManagerMock) GetByKindJSON(ctx context.Context, kind hub.RepositoryKind) ([]byte, error) { + args := m.Called(ctx, kind) + data, _ := args.Get(0).([]byte) + return data, args.Error(1) +} + // GetByName implements the RepositoryManager interface. func (m *ManagerMock) GetByName(ctx context.Context, name string) (*hub.Repository, error) { args := m.Called(ctx, name)