Skip to content

Commit

Permalink
resources endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
SimoneDutto committed Sep 4, 2024
1 parent b795e92 commit 1a882de
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 15 deletions.
16 changes: 16 additions & 0 deletions internal/common/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ func CreatePagination(sizeP, pageP *int, total int) (int, *int, LimitOffsetPagin
return page, nextPage, NewOffsetFilter(pageSize, offset)
}

// CreatePagination returns the current page, the page size, and the pagination.LimitOffsetPagination.
// This method is different approach to the method `CreatePagination` when we don't have the total number of records.
func CreatePaginationWithoutTotal(sizeP, pageP *int) (int, int, LimitOffsetPagination) {
pageSize := -1
offset := 0
page := 0

if sizeP != nil && pageP != nil {
pageSize = *sizeP
page = *pageP
offset = pageSize * page
}

return page, pageSize + 1, NewOffsetFilter(pageSize+1, offset)
}

type OpenFGAPagination struct {
limit int
token string
Expand Down
15 changes: 9 additions & 6 deletions internal/db/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"github.com/canonical/jimm/v3/internal/servermon"
)

const MUTIPLE_PAGE_SQL = `
// MULTI_TABLES_RAW_SQL contains the raw query fetching entities from multiple tables, with their respective entity parents.
const MULTI_TABLES_RAW_SQL = `
(
SELECT 'controller' AS type,
uuid AS id,
name AS name,
controllers.uuid AS id,
controllers.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
Expand Down Expand Up @@ -69,14 +70,16 @@ LIMIT ?;

type Resource struct {
Type string
UUID sql.NullString
ID sql.NullString
Name string
ParentId sql.NullString
ParentName string
ParentType string
}

func (d *Database) GetResources(ctx context.Context, limit, offset int) (_ []Resource, err error) {
// ListResources returns a list of models, clouds, controllers, service accounts, and application offers, with its respective parents.
// It has been implemented with a raw query because this is a specific implementation for the ReBAC Admin UI.
func (d *Database) ListResources(ctx context.Context, limit, offset int) (_ []Resource, err error) {
const op = errors.Op("db.GetMultipleModels")
if err := d.ready(); err != nil {
return nil, errors.E(op, err)
Expand All @@ -87,7 +90,7 @@ func (d *Database) GetResources(ctx context.Context, limit, offset int) (_ []Res
defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op))

db := d.DB.WithContext(ctx)
rows, err := db.Raw(MUTIPLE_PAGE_SQL, offset, limit).Rows()
rows, err := db.Raw(MULTI_TABLES_RAW_SQL, offset, limit).Rows()
if err != nil {
return nil, err
}
Expand Down
18 changes: 15 additions & 3 deletions internal/db/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/canonical/jimm/v3/internal/dbmodel"
)

func (s *dbSuite) Setup(c *qt.C) {
func (s *dbSuite) Setup(c *qt.C) (dbmodel.Model, dbmodel.Controller, dbmodel.Cloud) {
err := s.Database.Migrate(context.Background(), true)
c.Assert(err, qt.Equals, nil)

Expand Down Expand Up @@ -69,13 +69,25 @@ func (s *dbSuite) Setup(c *qt.C) {
}
err = s.Database.AddModel(context.Background(), &model)
c.Assert(err, qt.Equals, nil)
return model, controller, cloud
}

func (s *dbSuite) TestGetResources(c *qt.C) {
// create one model, one controller, one cloud
s.Setup(c)
model, controller, cloud := s.Setup(c)
ctx := context.Background()
res, err := s.Database.GetResources(ctx, 10, 0)
res, err := s.Database.ListResources(ctx, 10, 0)
c.Assert(err, qt.Equals, nil)
c.Assert(res, qt.HasLen, 3)
for _, r := range res {
switch r.Type {
case "model":
c.Assert(r.ID.String, qt.Equals, model.UUID.String)
c.Assert(r.ParentId.String, qt.Equals, controller.UUID)
case "controller":
c.Assert(r.ID.String, qt.Equals, controller.UUID)
case "cloud":
c.Assert(r.ID.String, qt.Equals, cloud.Name)
}
}
}
22 changes: 22 additions & 0 deletions internal/jimm/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2024 Canonical.
package jimm

import (
"context"

"github.com/canonical/jimm/v3/internal/common/pagination"
"github.com/canonical/jimm/v3/internal/db"
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/openfga"
)

// ListResources returns a list of resources known to JIMM with a pagination filter.
func (j *JIMM) ListResources(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]db.Resource, error) {
const op = errors.Op("jimm.GetResources")

if !user.JimmAdmin {
return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

return j.Database.ListResources(ctx, filter.Limit(), filter.Offset())
}
82 changes: 82 additions & 0 deletions internal/jimm/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2024 Canonical.
package jimm_test

import (
"context"
"slices"
"testing"
"time"

qt "github.com/frankban/quicktest"
"github.com/google/uuid"

"github.com/canonical/jimm/v3/internal/common/pagination"
"github.com/canonical/jimm/v3/internal/db"
"github.com/canonical/jimm/v3/internal/dbmodel"
"github.com/canonical/jimm/v3/internal/jimm"
"github.com/canonical/jimm/v3/internal/jimmtest"
"github.com/canonical/jimm/v3/internal/openfga"
)

func TestGetResources(t *testing.T) {
c := qt.New(t)
ctx := context.Background()
ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name())
c.Assert(err, qt.IsNil)

now := time.Now().UTC().Round(time.Millisecond)
j := &jimm.JIMM{
UUID: uuid.NewString(),
Database: db.Database{
DB: jimmtest.PostgresDB(c, func() time.Time { return now }),
},
OpenFGAClient: ofgaClient,
}

err = j.Database.Migrate(ctx, false)
c.Assert(err, qt.IsNil)
user, _, controller, model, applicationOffer, cloud, _ := createTestControllerEnvironment(ctx, c, j.Database)

ids := []string{user.Name, controller.UUID, model.UUID.String, applicationOffer.UUID, cloud.Name}
slices.Sort(ids)

u := openfga.NewUser(&dbmodel.Identity{Name: "[email protected]"}, ofgaClient)
u.JimmAdmin = true

testCases := []struct {
desc string
limit int
offset int
identities []string
}{
{
desc: "test with first resources",
limit: 3,
offset: 0,
identities: []string{ids[0], ids[1], ids[2]},
},
{
desc: "test with remianing ids",
limit: 3,
offset: 3,
identities: []string{ids[3], ids[4]},
},
{
desc: "test out of range",
limit: 3,
offset: 6,
identities: []string{},
},
}
for _, t := range testCases {
c.Run(t.desc, func(c *qt.C) {
filter := pagination.NewOffsetFilter(t.limit, t.offset)
resources, err := j.ListResources(ctx, u, filter)
c.Assert(err, qt.IsNil)
c.Assert(resources, qt.HasLen, len(t.identities))
for i := range len(t.identities) {
c.Assert(resources[i].ID.String, qt.Equals, t.identities[i])
}
})
}
}
7 changes: 7 additions & 0 deletions internal/jimmtest/jimm_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type JIMM struct {
InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error)
InitiateInternalMigration_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error)
ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error)
ListResources_ func(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]db.Resource, error)
Offer_ func(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error
PubSubHub_ func() *pubsub.Hub
PurgeLogs_ func(ctx context.Context, user *openfga.User, before time.Time) (int64, error)
Expand Down Expand Up @@ -300,6 +301,12 @@ func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, fi
}
return j.ListApplicationOffers_(ctx, user, filters...)
}
func (j *JIMM) ListResources(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]db.Resource, error) {
if j.ListResources_ == nil {
return nil, errors.E(errors.CodeNotImplemented)
}
return j.ListResources_(ctx, user, filter)
}
func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error {
if j.Offer_ == nil {
return errors.E(errors.CodeNotImplemented)
Expand Down
5 changes: 3 additions & 2 deletions internal/jujuapi/controllerroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type JIMM interface {
AddHostedCloud(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error
AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error
CopyServiceAccountCredential(ctx context.Context, u *openfga.User, svcAcc *openfga.User, cloudCredentialTag names.CloudCredentialTag) (names.CloudCredentialTag, []jujuparams.UpdateCredentialModelResult, error)
CountIdentities(ctx context.Context, user *openfga.User) (int, error)
DestroyOffer(ctx context.Context, user *openfga.User, offerURL string, force bool) error
FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error)
FindAuditEvents(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error)
Expand All @@ -51,10 +52,8 @@ type JIMM interface {
GetCloudCredentialAttributes(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error)
GetCredentialStore() credentials.CredentialStore
GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error)
ListIdentities(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]openfga.User, error)
// FetchIdentity finds the user in jimm or returns a not-found error
FetchIdentity(ctx context.Context, username string) (*openfga.User, error)
CountIdentities(ctx context.Context, user *openfga.User) (int, error)
GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error)
GetUserControllerAccess(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error)
GetUserModelAccess(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error)
Expand All @@ -66,6 +65,8 @@ type JIMM interface {
InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error)
InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error)
ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error)
ListIdentities(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]openfga.User, error)
ListResources(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]db.Resource, error)
Offer(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error
PubSubHub() *pubsub.Hub
PurgeLogs(ctx context.Context, user *openfga.User, before time.Time) (int64, error)
Expand Down
1 change: 1 addition & 0 deletions internal/rebac_admin/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package rebac_admin
var (
NewGroupService = newGroupService
NewidentitiesService = newidentitiesService
NewResourcesService = newResourcesService
)

type GroupsService = groupsService
36 changes: 32 additions & 4 deletions internal/rebac_admin/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (

"github.com/canonical/rebac-admin-ui-handlers/v1/resources"

"github.com/canonical/jimm/v3/internal/common/pagination"
"github.com/canonical/jimm/v3/internal/jujuapi"
"github.com/canonical/jimm/v3/internal/rebac_admin/utils"
)

// resourcesService implements the `resourcesService` interface.
type resourcesService struct {
jimm jujuapi.JIMM
}
Expand All @@ -22,11 +22,39 @@ func newResourcesService(jimm jujuapi.JIMM) *resourcesService {
}
}

// resourcesService defines an abstract backend to handle Resources related operations.
// ListResources returns a page of Resource objects of at least `size` elements if available.
func (s *resourcesService) ListResources(ctx context.Context, params *resources.GetResourcesParams) (*resources.PaginatedResponse[resources.Resource], error) {
_, err := utils.GetUserFromContext(ctx)
user, err := utils.GetUserFromContext(ctx)
if err != nil {
return nil, err
}
return nil, nil
page, pageSize, pagination := pagination.CreatePaginationWithoutTotal(params.Size, params.Page)
res, err := s.jimm.ListResources(ctx, user, pagination)
if err != nil {
return nil, err
}
// We fetch one record more than the page size. Then, we set the next page if we have this many records.
// Otherwise next page is empty.
var nextPage *int
if len(res) == pageSize {
nPage := page + 1
nextPage = &nPage
res = res[:len(res)-1]
}
rRes := make([]resources.Resource, len(res))
for i, u := range res {
rRes[i] = utils.FromDbResourcesToResources(u)
}

return &resources.PaginatedResponse[resources.Resource]{
Data: rRes,
Meta: resources.ResponseMeta{
Page: &page,
Size: len(rRes),
Total: nil,
},
Next: resources.Next{
Page: nextPage,
},
}, nil
}
Loading

0 comments on commit 1a882de

Please sign in to comment.