Skip to content

Commit

Permalink
Css 9402/resources (#1347)
Browse files Browse the repository at this point in the history
* resources endpoint
  • Loading branch information
SimoneDutto committed Sep 4, 2024
1 parent 8766152 commit f0c184b
Show file tree
Hide file tree
Showing 13 changed files with 592 additions and 10 deletions.
31 changes: 23 additions & 8 deletions internal/common/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,37 @@ func (l LimitOffsetPagination) Offset() int {
}

// CreatePagination returns the current page, the next page if exists, and the pagination.LimitOffsetPagination.
func CreatePagination(sizeP, pageP *int, total int) (int, *int, LimitOffsetPagination) {
func CreatePagination(sizeP, pageP *int, total int) (currentPage int, nextPage *int, _ LimitOffsetPagination) {
pageSize := -1
offset := 0
page := 0
var nextPage *int

if sizeP != nil && pageP != nil {
pageSize = *sizeP
page = *pageP
offset = pageSize * page
currentPage = *pageP
offset = pageSize * currentPage
}
if (page+1)*pageSize < total {
nPage := page + 1
if (currentPage+1)*pageSize < total {
nPage := currentPage + 1
nextPage = &nPage
}
return page, nextPage, NewOffsetFilter(pageSize, offset)
return currentPage, nextPage, NewOffsetFilter(pageSize, offset)
}

// CreatePagination returns the current page, the expected 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.
// We return the expectedPageSize, which is pageSize +1, so we fetch one record more from the db.
// We then check the resulting records are enough to advice the consumers to ask for one more page or not.
func CreatePaginationWithoutTotal(sizeP, pageP *int) (currentPage int, expectedPageSize int, _ LimitOffsetPagination) {
pageSize := -1
offset := 0

if sizeP != nil && pageP != nil {
pageSize = *sizeP
currentPage = *pageP
offset = pageSize * currentPage
}
expectedPageSize = pageSize + 1
return currentPage, expectedPageSize, NewOffsetFilter(pageSize+1, offset)
}

type OpenFGAPagination struct {
Expand Down
12 changes: 12 additions & 0 deletions internal/common/pagination/pagination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ func TestCreatePagination(t *testing.T) {
}
}

// test the requested size is 1 more than then page size.
func TestCreatePaginationWithoutTotal(t *testing.T) {
c := qt.New(t)
pPage := utils.IntToPointer(0)
pSize := utils.IntToPointer(10)
page, size, pag := pagination.CreatePaginationWithoutTotal(pSize, pPage)
c.Assert(page, qt.Equals, 0)
c.Assert(pag.Limit(), qt.Equals, 11)
c.Assert(pag.Offset(), qt.Equals, 0)
c.Assert(size, qt.Equals, 11)
}

func TestTokenFilter(t *testing.T) {
testToken := "test-token"
testCases := []struct {
Expand Down
108 changes: 108 additions & 0 deletions internal/db/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2024 Canonical.
package db

import (
"context"
"database/sql"

"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/servermon"
)

// RESOURCES_RAW_SQL contains the raw query fetching entities from multiple tables, with their respective entity parents.
const RESOURCES_RAW_SQL = `
(
SELECT 'application_offer' AS type,
application_offers.uuid AS id,
application_offers.name AS name,
models.uuid AS parent_id,
models.name AS parent_name,
'model' AS parent_type
FROM application_offers
JOIN models ON application_offers.model_id = models.id
)
UNION
(
SELECT 'cloud' AS type,
clouds.name AS id,
clouds.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
FROM clouds
)
UNION
(
SELECT 'controller' AS type,
controllers.uuid AS id,
controllers.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
FROM controllers
)
UNION
(
SELECT 'model' AS type,
models.uuid AS id,
models.name AS name,
controllers.uuid AS parent_id,
controllers.name AS parent_name,
'controller' AS parent_type
FROM models
JOIN controllers ON models.controller_id = controllers.id
)
UNION
(
SELECT 'service_account' AS type,
identities.name AS id,
identities.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
FROM identities
WHERE name LIKE '%@serviceaccount'
)
ORDER BY type, id
OFFSET ?
LIMIT ?;
`

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

// 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.ListResources")
if err := d.ready(); err != nil {
return nil, errors.E(op, err)
}

durationObserver := servermon.DurationObserver(servermon.DBQueryDurationHistogram, string(op))
defer durationObserver()
defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op))

db := d.DB.WithContext(ctx)
rows, err := db.Raw(RESOURCES_RAW_SQL, offset, limit).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
resources := make([]Resource, 0)
for rows.Next() {
var res Resource
err := db.ScanRows(rows, &res)
if err != nil {
return nil, err
}
resources = append(resources, res)
}
return resources, nil
}
95 changes: 95 additions & 0 deletions internal/db/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2024 Canonical.
package db_test

import (
"context"
"database/sql"

qt "github.com/frankban/quicktest"
"github.com/juju/juju/state"

"github.com/canonical/jimm/v3/internal/db"
"github.com/canonical/jimm/v3/internal/dbmodel"
)

func SetupDB(c *qt.C, database *db.Database) (dbmodel.Model, dbmodel.Controller, dbmodel.Cloud) {
u, err := dbmodel.NewIdentity("[email protected]")
c.Assert(err, qt.IsNil)
c.Assert(database.DB.Create(&u).Error, qt.IsNil)

cloud := dbmodel.Cloud{
Name: "test-cloud",
Type: "test-provider",
Regions: []dbmodel.CloudRegion{{
Name: "test-region",
}},
}
c.Assert(database.DB.Create(&cloud).Error, qt.IsNil)

cred := dbmodel.CloudCredential{
Name: "test-cred",
Cloud: cloud,
Owner: *u,
AuthType: "empty",
}
c.Assert(database.DB.Create(&cred).Error, qt.IsNil)

controller := dbmodel.Controller{
Name: "test-controller",
UUID: "00000000-0000-0000-0000-0000-0000000000001",
CloudName: "test-cloud",
CloudRegion: "test-region",
}
err = database.AddController(context.Background(), &controller)
c.Assert(err, qt.Equals, nil)

model := dbmodel.Model{
Name: "test-model-1",
UUID: sql.NullString{
String: "00000001-0000-0000-0000-0000-000000000001",
Valid: true,
},
OwnerIdentityName: u.Name,
ControllerID: controller.ID,
CloudRegionID: cloud.Regions[0].ID,
CloudCredentialID: cred.ID,
Type: "iaas",
DefaultSeries: "warty",
Life: state.Alive.String(),
Status: dbmodel.Status{
Status: "available",
Since: db.Now(),
},
SLA: dbmodel.SLA{
Level: "unsupported",
},
}
err = database.AddModel(context.Background(), &model)
c.Assert(err, qt.Equals, nil)
return model, controller, cloud
}

func (s *dbSuite) TestGetResources(c *qt.C) {
ctx := context.Background()
err := s.Database.Migrate(context.Background(), true)
c.Assert(err, qt.Equals, nil)
res, err := s.Database.ListResources(ctx, 10, 0)
c.Assert(err, qt.Equals, nil)
c.Assert(res, qt.HasLen, 0)
// create one model, one controller, one cloud
model, controller, cloud := SetupDB(c, s.Database)
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.ListResources")

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

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

import (
"context"
"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)
_, _, controller, model, applicationOffer, cloud, _ := createTestControllerEnvironment(ctx, c, j.Database)

ids := []string{applicationOffer.UUID, cloud.Name, controller.UUID, model.UUID.String}

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]},
},
{
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
Loading

0 comments on commit f0c184b

Please sign in to comment.