Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merge from v3 to feature-http-proxy #1368

Merged
merged 13 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/test-server/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ runs:
uses: charmed-kubernetes/actions-operator@main
with:
provider: "lxd"
channel: "5.19/stable"
channel: "5.21/stable"
juju-channel: ${{ inputs.juju-channel }}
bootstrap-options: "--config ${{ github.action_path }}/../../../cloudinit.temp.yaml --config login-token-refresh-url=https://jimm.localhost/.well-known/jwks.json"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: stable
go-version-file: 'go.mod'

- name: Run Golangci-lint
uses: golangci/golangci-lint-action@v6
Expand Down
5 changes: 4 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ linters-settings:
gocognit:
min-complexity: 30
goheader:
values:
regexp:
year: (\d{4})
template: |-
Copyright 2024 Canonical.
Copyright {{year}} Canonical.
importas:
no-unaliased: false
no-extra-aliases: false
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ require (
require (
github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a
github.com/canonical/ofga v0.10.0
github.com/canonical/rebac-admin-ui-handlers v0.1.0
github.com/canonical/rebac-admin-ui-handlers v0.1.1
github.com/coreos/go-oidc/v3 v3.9.0
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
github.com/go-chi/chi/v5 v5.0.12
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ github.com/canonical/ofga v0.10.0 h1:DHXhG/DAXWWQT/I+2jzr4qm0uTIYrILmtMxd6ZqmEzE
github.com/canonical/ofga v0.10.0/go.mod h1:u4Ou8dbIhO7FmVlT7W3rX2roD9AOGz/CqmGh7AdF0Lo=
github.com/canonical/rebac-admin-ui-handlers v0.1.0 h1:Bef1N/RgQine8hHX4ZMksQz/1VKsy4DHK2XdhAzQsZs=
github.com/canonical/rebac-admin-ui-handlers v0.1.0/go.mod h1:EIdBoaTHWYPkzNeUeXUBueJkglN9nQz5HLIvaOT7o1k=
github.com/canonical/rebac-admin-ui-handlers v0.1.1 h1:rjsb45diShhwD/uUFpai6gmhFUzT+jTdsnEWcOvcKx4=
github.com/canonical/rebac-admin-ui-handlers v0.1.1/go.mod h1:EIdBoaTHWYPkzNeUeXUBueJkglN9nQz5HLIvaOT7o1k=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
Expand Down
4 changes: 4 additions & 0 deletions internal/common/utils/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ package utils
func IntToPointer(i int) *int {
return &i
}

func StringToPointer(s string) *string {
return &s
}
175 changes: 119 additions & 56 deletions internal/db/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,65 @@ import (
"context"
"database/sql"

"gorm.io/gorm"

"github.com/canonical/jimm/v3/internal/dbmodel"
"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'
)
const ApplicationOffersQueryKey = "application_offers"
const selectApplicationOffers = `
'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
`

const CloudsQueryKey = "clouds"
const selectClouds = `
'cloud' AS type,
clouds.name AS id,
clouds.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
`

const ControllersQueryKey = "controllers"
const selectControllers = `
'controller' AS type,
controllers.uuid AS id,
controllers.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
`

const ModelsQueryKey = "models"
const selectModels = `
'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
`

const ServiceAccountQueryKey = "identities"
const selectIdentities = `
'service_account' AS type,
identities.name AS id,
identities.name AS name,
'' AS parent_id,
'' AS parent_name,
'' AS parent_type
`

const unionQuery = `
? UNION ? UNION ? UNION ? UNION ?
ORDER BY type, id
OFFSET ?
LIMIT ?;
Expand All @@ -79,7 +80,7 @@ type Resource struct {

// 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) {
func (d *Database) ListResources(ctx context.Context, limit, offset int, namePrefixFilter, typeFilter string) (_ []Resource, err error) {
const op = errors.Op("db.ListResources")
if err := d.ready(); err != nil {
return nil, errors.E(op, err)
Expand All @@ -90,7 +91,11 @@ func (d *Database) ListResources(ctx context.Context, limit, offset int) (_ []Re
defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op))

db := d.DB.WithContext(ctx)
rows, err := db.Raw(RESOURCES_RAW_SQL, offset, limit).Rows()
query, err := buildQuery(db, offset, limit, namePrefixFilter, typeFilter)
if err != nil {
return nil, err
}
rows, err := query.Rows()
if err != nil {
return nil, err
}
Expand All @@ -106,3 +111,61 @@ func (d *Database) ListResources(ctx context.Context, limit, offset int) (_ []Re
}
return resources, nil
}

// buildQuery is a utility function to build the database query according to two optional parameters.
// namePrefixFilter: used to match resources name prefix.
// typeFilter: used to match resources type. If this is not empty the resources are fetched from a single table.
func buildQuery(db *gorm.DB, offset, limit int, namePrefixFilter, typeFilter string) (*gorm.DB, error) {
applicationOffersQuery := db.Select(selectApplicationOffers).
Model(&dbmodel.ApplicationOffer{}).
Where("(CASE WHEN ? = '' THEN TRUE ELSE application_offers.name LIKE ? END)", namePrefixFilter, namePrefixFilter+"%").
Joins("JOIN models ON application_offers.model_id = models.id")

cloudsQuery := db.Select(selectClouds).
Model(&dbmodel.Cloud{}).
Where("(CASE WHEN ? = '' THEN TRUE ELSE clouds.name LIKE ? END)", namePrefixFilter, namePrefixFilter+"%")

controllersQuery := db.Select(selectControllers).
Model(&dbmodel.Controller{}).
Where("(CASE WHEN ? = '' THEN TRUE ELSE controllers.name LIKE ? END)", namePrefixFilter, namePrefixFilter+"%")

modelsQuery := db.Select(selectModels).
Model(&dbmodel.Model{}).
Where("(CASE WHEN ? = '' THEN TRUE ELSE models.name LIKE ? END)", namePrefixFilter, namePrefixFilter+"%").
Joins("JOIN controllers ON models.controller_id = controllers.id")

serviceAccountsQuery := db.Select(selectIdentities).
Model(&dbmodel.Identity{}).
Where("name LIKE '%@serviceaccount' AND (CASE WHEN ? = '' THEN TRUE ELSE identities.name LIKE ? END)", namePrefixFilter, namePrefixFilter+"%")

// if the typeFilter is set we only return the query for that specif entityType, otherwise the union.
if typeFilter == "" {
return db.
Raw(unionQuery,
applicationOffersQuery,
cloudsQuery,
controllersQuery,
modelsQuery,
serviceAccountsQuery,
offset,
limit,
), nil
}
var query *gorm.DB
switch typeFilter {
case ControllersQueryKey:
query = controllersQuery
case CloudsQueryKey:
query = cloudsQuery
case ApplicationOffersQueryKey:
query = applicationOffersQuery
case ModelsQueryKey:
query = modelsQuery
case ServiceAccountQueryKey:
query = serviceAccountsQuery
default:
// this shouldn't happen because we have validated the entityFilter at API layer
return nil, errors.E("this entityType does not exist")
}
return query.Order("id").Offset(offset).Limit(limit), nil
}
103 changes: 97 additions & 6 deletions internal/db/resource_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 SetupDB(c *qt.C, database *db.Database) (dbmodel.Model, dbmodel.Controller, dbmodel.Cloud) {
func SetupDB(c *qt.C, database *db.Database) (dbmodel.Model, dbmodel.Controller, dbmodel.Cloud, dbmodel.Identity) {
u, err := dbmodel.NewIdentity("[email protected]")
c.Assert(err, qt.IsNil)
c.Assert(database.DB.Create(&u).Error, qt.IsNil)
Expand Down Expand Up @@ -66,21 +66,27 @@ func SetupDB(c *qt.C, database *db.Database) (dbmodel.Model, dbmodel.Controller,
}
err = database.AddModel(context.Background(), &model)
c.Assert(err, qt.Equals, nil)
return model, controller, cloud
clientIDWithDomain := "abda51b2-d735-4794-a8bd-49c506baa4af@serviceaccount"
sa, err := dbmodel.NewIdentity(clientIDWithDomain)
c.Assert(err, qt.Equals, nil)
err = database.GetIdentity(context.Background(), sa)
c.Assert(err, qt.Equals, nil)

return model, controller, cloud, *sa
}

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)
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)
model, controller, cloud, sva := SetupDB(c, s.Database)
res, err = s.Database.ListResources(ctx, 10, 0, "", "")
c.Assert(err, qt.Equals, nil)
c.Assert(res, qt.HasLen, 3)
c.Assert(res, qt.HasLen, 4)
for _, r := range res {
switch r.Type {
case "model":
Expand All @@ -90,6 +96,91 @@ func (s *dbSuite) TestGetResources(c *qt.C) {
c.Assert(r.ID.String, qt.Equals, controller.UUID)
case "cloud":
c.Assert(r.ID.String, qt.Equals, cloud.Name)
case "service_account":
c.Assert(r.ID.String, qt.Equals, sva.Name)
}
}
}

func (s *dbSuite) TestGetResourcesWithNameTypeFilter(c *qt.C) {
ctx := context.Background()
err := s.Database.Migrate(context.Background(), true)
c.Assert(err, qt.Equals, nil)
// create one model, one controller, one cloud
model, controller, cloud, sva := SetupDB(c, s.Database)

tests := []struct {
description string
nameFilter string
typeFilter string
limit int
offset int
expectedSize int
expectedUUIDs []string
}{
{
description: "filter on model name",
nameFilter: model.Name,
limit: 10,
offset: 0,
typeFilter: "",
expectedSize: 1,
expectedUUIDs: []string{model.UUID.String},
},
{
description: "filter name test prefix",
nameFilter: "test",
limit: 10,
offset: 0,
typeFilter: "",
expectedSize: 3,
expectedUUIDs: []string{cloud.Name, controller.UUID, model.UUID.String},
},
{
description: "filter name controller suffix",
nameFilter: "controller",
limit: 10,
offset: 0,
typeFilter: "",
expectedSize: 0,
expectedUUIDs: []string{},
},
{
description: "filter only models",
nameFilter: "test",
limit: 10,
offset: 0,
typeFilter: "models",
expectedSize: 1,
expectedUUIDs: []string{model.UUID.String},
},
{
description: "filter only service accounts",
nameFilter: "",
limit: 10,
offset: 0,
typeFilter: "identities",
expectedSize: 1,
expectedUUIDs: []string{sva.Name},
},
{
description: "filter only service accounts and name",
nameFilter: "not-found",
limit: 10,
offset: 0,
typeFilter: "identities",
expectedSize: 0,
expectedUUIDs: []string{},
},
}
for _, t := range tests {
c.Run(t.description, func(c *qt.C) {
res, err := s.Database.ListResources(ctx, t.limit, t.offset, t.nameFilter, t.typeFilter)
c.Assert(err, qt.Equals, nil)
c.Assert(res, qt.HasLen, t.expectedSize)
for i, r := range res {
c.Assert(r.ID.String, qt.Equals, t.expectedUUIDs[i])
}
})
}
}
Loading
Loading