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

refactor(internal/jimm/jimm.go): introduces a jimm constructor #1468

Merged
merged 2 commits into from
Dec 6, 2024

Conversation

alesstimec
Copy link
Collaborator

@alesstimec alesstimec commented Nov 26, 2024

Description

Introduces a constructor for jimm in internal/jimm. With the lightest touch possible atm.

drive-by fix: the db.Database type holds a migrated (uint32) field. however we were passing the database around by value and every time we copied it, whoever tried to use it would get a CodeUpgradeInProgress error. In this PR we pass a pointer to the database around, hopefully avoiding further migration voes.

Engineering checklist

Check only items that apply

  • Documentation updated
  • Covered by unit tests
  • Covered by integration tests

Test instructions

Notes for code reviewers

@alesstimec alesstimec requested a review from a team as a code owner November 26, 2024 15:37
@alesstimec alesstimec force-pushed the internal-jimm-contructor branch 5 times, most recently from a730c54 to b910d95 Compare November 27, 2024 14:49
cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
internal/jimm/jimm.go Outdated Show resolved Hide resolved
internal/jimm/jimm.go Outdated Show resolved Hide resolved
// connections therefore the JIMM object itself does not contain any per-
// request state.
type JIMM struct {
Parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are embedding the struct, it's not Parameters but more Services. Because we don't start the service in the constructor, we rather take the service reference and store it in the JIMM struct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constructors have parameters. and the constructor will then instantiate all the managers/service provided by JIMM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't embed it, params you pass into the func, and on JIMM it filles in another struct. Don't couple them please?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're only embedded for the time being. Once we have the domain objects, we can delete this.

Comment on lines 177 to 258
func New(p Parameters, options ...Option) (*JIMM, error) {
j := &JIMM{
Parameters: p,
}

if err := j.Database.Migrate(context.Background(), false); err != nil {
return nil, errors.E(err)
}

roleManager, err := role.NewRoleManager(j.Database, p.OpenFGAClient)
if err != nil {
return nil, err
}
j.RoleManager = roleManager
for _, option := range options {
option(j)
}

return j, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought this method would have been a method where you receive some parameters (ex. db connection string), and you create the Database, Openfgaclient.
So we'll end up with a central point where we validate jimm's config, start the jimm services and return the well defined jimm struct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's correct like this, the JIMM method sets up the business logic, so when we have more "managers", the JIMM struct won't need any of the fields in Params, it will just take the pieces of params like the DB, OpenFGA client, pubSub, etc and pass them to the relevant constructors for the managers. Whoever calls jimm.New() decides what they want to use as a DB, as an auth service, etc (even if you can't mock the DB or auth service today).

Copy link
Contributor

@kian99 kian99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good with some comments

cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
internal/jimm/admin_test.go Outdated Show resolved Hide resolved
internal/jimm/cloud.go Show resolved Hide resolved
internal/jimm/jimm.go Outdated Show resolved Hide resolved
@alesstimec alesstimec force-pushed the internal-jimm-contructor branch 2 times, most recently from f0bc5ed to cd1f2a2 Compare November 28, 2024 15:03
Copy link

⚠️ This PR contains unsigned commits. To get your PR merged, please sign those commits (git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}) and force push them to this branch (git push --force-with-lease).

If you're new to commit signing, there are different ways to set it up:

Sign commits with gpg

Follow the steps below to set up commit signing with gpg:

  1. Generate a GPG key
  2. Add the GPG key to your GitHub account
  3. Configure git to use your GPG key for commit signing
Sign commits with ssh-agent

Follow the steps below to set up commit signing with ssh-agent:

  1. Generate an SSH key and add it to ssh-agent
  2. Add the SSH key to your GitHub account
  3. Configure git to use your SSH key for commit signing
Sign commits with 1Password

You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.

Learn how to use 1Password to sign your commits.

Watch the demo

@alesstimec alesstimec force-pushed the internal-jimm-contructor branch 3 times, most recently from abc2bf1 to 30ce643 Compare December 3, 2024 07:53
cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
}

sqlDb, err := s.jimm.Database.DB.DB()
sqlDb, err := db.DB.DB()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we improve this at the moment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

improve how?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a separate PR, and not for you per se, but I was just remarking that the naming isn't great. database.database.database() is crazy.

cmd/jimmsrv/service/service_test.go Outdated Show resolved Hide resolved
internal/jimm/jimm.go Outdated Show resolved Hide resolved
internal/jimm/jimm.go Outdated Show resolved Hide resolved
internal/jimm/jimm.go Outdated Show resolved Hide resolved
cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved
Copy link
Contributor

@SimoneDutto SimoneDutto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few comments then LGTM

cmd/jimmsrv/service/service.go Outdated Show resolved Hide resolved

jimmParameters := jimm.Parameters{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would still call the services. But I won't die on this hill

internal/jimm/jimm.go Outdated Show resolved Hide resolved
@alesstimec alesstimec force-pushed the internal-jimm-contructor branch 10 times, most recently from f0985ed to fbe23f4 Compare December 5, 2024 08:17
internal/jimm/jimm.go Outdated Show resolved Hide resolved
return errors.E("missing jwks service")
}

if p.JWTService == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this should only contain primitives? Not Serices?

// connections therefore the JIMM object itself does not contain any per-
// request state.
type JIMM struct {
Parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't embed it, params you pass into the func, and on JIMM it filles in another struct. Don't couple them please?

@alesstimec alesstimec force-pushed the internal-jimm-contructor branch from 40f3c7d to 61fea0d Compare December 5, 2024 09:51
- adds jimm.New costructor
- adds jimmtest.NewJIMM
- refactors GetRoleManager and GetGroupManager
@alesstimec alesstimec force-pushed the internal-jimm-contructor branch from 61fea0d to 8b40a37 Compare December 5, 2024 10:22
internal/jimm/access_test.go Show resolved Hide resolved
Comment on lines +72 to +85
if p.Database == nil {
p.Database = &db.Database{
DB: PostgresDB(t, func() time.Time { return now }),
}
}
if p.CredentialStore == nil {
p.CredentialStore = p.Database
}
if p.OpenFGAClient == nil {
ofgaClient, _, _, err := SetupTestOFGAClient(t.Name())
if err != nil {
t.Fatalf("setting up openfga client: %v", err)
}
p.OpenFGAClient = ofgaClient
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, this is the default config for jimmtest now. Update the godoc please

}
)

func NewJIMM(t Tester, additionalParameters *jimm.Parameters, options ...Option) *jimm.JIMM {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

godoc

internal/testutils/jimmtest/jimm.go Show resolved Hide resolved
Copy link
Contributor

@SimoneDutto SimoneDutto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, but please new time let's split this in multiple pr, with a feature branch.
Too many mechanical changes in the same pr

Comment on lines +18 to +24
type Option func(j *jimm.JIMM)

var (
UnsetCredentialStore Option = func(j *jimm.JIMM) {
j.CredentialStore = nil
}
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we give default options to choose from, do we need to have the Option exported?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. we have a slight bug around cloud credentials. and there is a test that relies on the fact that the credential store is unset.. until we fix that bug, we need this option

@@ -47,7 +46,11 @@ func (t GocheckTester) Name() string {
}

func (t GocheckTester) Cleanup(f func()) {
t.C.Logf("warning: gocheck does not support Cleanup functions; make sure you're using suite's tear-down method")
if t.AddCleanup != nil {
t.AddCleanup(f)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why bother with adding the cleanup? Since we don't have any cleanup with all the ŧesting.t tests.

I think it's just enough to increase the conn limit on postgres

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, where did the change go? Or now it's fine because we are not creating that many connections?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this link stops working when you force push btw.

Copy link
Contributor

@kian99 kian99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's almost okay. My biggest gripe is with all tests getting a DB and OpenFGA client.

}

sqlDb, err := s.jimm.Database.DB.DB()
sqlDb, err := db.DB.DB()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a separate PR, and not for you per se, but I was just remarking that the naming isn't great. database.database.database() is crazy.

@@ -70,7 +70,7 @@ services:
POSTGRES_PASSWORD: jimm
# Since it's mainly used for testing purposes, it's okay to set fsync=off for
# improved performance.
command: -c fsync=off -c full_page_writes=off
command: -c fsync=off -c full_page_writes=off -c max_connections=200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit concerning. I wonder if we're going over the default 100 connections because we're leaking connections or because of tests running in parallel. Perhaps one way to check would be to add a global test cleanup methods that checks the number of open connections to Postgres. Or run tests with -p 1 and print the number of connections to Postgres between each test incovation.

// connections therefore the JIMM object itself does not contain any per-
// request state.
type JIMM struct {
Parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're only embedded for the time being. Once we have the domain objects, we can delete this.


if p.Database == nil {
p.Database = &db.Database{
DB: PostgresDB(t, func() time.Time { return now }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently not in favour of this approach where every test that calls jimmtest.NewJIMM gets a whole database and OpenFGA store. Some tests need neither, some tests need one. That was nicely captured in the tests and when we move to managers we can use suites so that there is less duplication of that boilerplate test setup.

I think the default should be all mocks or all nil fields unless the test requests them.

@@ -172,14 +190,14 @@ func (s *JIMMSuite) TearDownTest(c *gc.C) {
}
}

func (s *JIMMSuite) setupMacaroonDischarger(c *gc.C) *discharger.MacaroonDischarger {
func setupMacaroonDischarger(c *gc.C, uuid string, db *db.Database, ofgaClient *openfga.OFGAClient) *discharger.MacaroonDischarger {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed to a function?

}

if _, err := url.Parse(p.DashboardFinalRedirectURL); err != nil {
return nil, errors.E(op, err, "failed to parse final redirect url for the dashboard")
}

rebacBackend, err := rebac_admin.SetupBackend(ctx, &s.jimm)
// instantiate jimm
s.jimm, err = jimm.New(jimmParameters)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with this as is, as we discussed, but we should definitely break this down further into primitives and application domains later on. I'd love to see primitive db, openfga, authsvc (as params) -> and within the constructor: rolemanager, identityservice, etc. all constructed returning a nicely complete jimm. And, if we want to override any application domain we pass an option.

)

if p.AuditLogRetentionPeriodInDays != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should have service params honestly, it's confusing / bit odd, to see params which re for both jimm and the service and they take effect when you start the service. @kian99 I think you mentioned this right?

return s, nil
}

func (s *Service) StartServices(ctx context.Context, svc *service.Service) {
// on the leader unit we start additional routines
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like how we start all services here, but in main.go we're running this and then we start the jimm service. Would be nice if jimm was also started here? Could even setup the on shutdown etc all here.

@@ -244,7 +244,7 @@ controllers:
region: test-region
priority: 1
`)
env.PopulateDB(c, *s.Database)
env.PopulateDB(c, s.Database)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting we could handle ** here. Good spot.


var now = (time.Time{}).UTC().Round(time.Millisecond)

type Option func(j *jimm.JIMM)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, we use option pattern only in the context of tests... I think... It's more useful the have the pattern on the JIMM constructor for when we come to (or not to) initialise services. Kinda like juju's manifolds where they configure the binary in different ways - if we ever need to go down that path we can. It's also no harm I think rather than having the option pattern only apply to the jimmtest constructor, I think it's a lot clearer if we just use jimm.New() and pass test options.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope.. in production we never expect to use options.. only for tests, so why pollute production code with options?

Copy link
Contributor

@ale8k ale8k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, I've left some comments. Nothing too serious, but more about options and service. I'd much prefer we just use a single constructor for initialisng jimm in tests and in main.

@alesstimec alesstimec merged commit 4b9eaf2 into canonical:v3 Dec 6, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants