-
Notifications
You must be signed in to change notification settings - Fork 55
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
Replace gorm.DB with an interface #3008
Conversation
s := []data.SelectorFunc{ | ||
data.Preload("IssuedForIdentity"), | ||
data.ByOptionalIssuedFor(identityID), | ||
data.ByOptionalName(name), | ||
} | ||
if !showExpired { | ||
s = append(s, data.ByNotExpiredOrExtended()) | ||
} | ||
|
||
return data.ListAccessKeys(db.Preload("IssuedForIdentity"), p, s...) | ||
return data.ListAccessKeys(db, p, s...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a few places I had to convert Preload
to a selector so that I could accept a data.GormTxn
as the first argument to the query function, and only selectors would continue to use gorm.DB
. This is one of the places, there should be a few others.
db = data.NewTransaction(db.GormDB(), details.Org.ID) | ||
c.Set("db", db) | ||
rCtx := GetRequestContext(c) | ||
rCtx.DBTxn = db | ||
c.Set(RequestContextKey, rCtx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we're passing orgID as part of the transaction the data.CreateOrganization
function can't set the org ID in the context. I think this is a good thing, as previously it was surprising that creating an org mutated the DB to make all queries use that org.
As a consequence of that change we have to set the tx here for the rest of the request, but this is the only place where we need to do this.
I've also restored the original name for CreateOrganization
, now that it's not doing the SetContext
part.
// FIXME: this is a hack to keep our tests passing. The db should not | ||
// be scoped to an org ID. | ||
return d.DefaultOrg.ID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling this out. This is not a regression, because it works this way on main
, but it is a problem we should fix.
All of our test fixtures should be more explicit about which org they are using when creating things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this one. How does this do the right thing in production?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. This is why I prioritized this part of the data
package work, because this is the behaviour we have now by setting the context when we create a database.
It "works" in production because requests use the transaction reference where we override the organization value.
The only code that doesn't do that right now is:
- the config loading, where you fixed it to set this same default value
- tests in the
server
package where we do the same thing by setting the default org.
I plan on removing this in the first follow up PR.
@@ -76,66 +75,8 @@ func TestSnowflakeIDSerialization(t *testing.T) { | |||
}) | |||
} | |||
|
|||
func TestDatabaseSelectors(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed because this test is just a simulation of the selector behaviour, and we should be removing selector functions soon. Porting this test to the new interface would be a lot more work than other tests, and is not worth it.
selectID := func(db *gorm.DB) *gorm.DB { | ||
return db.Select("id") | ||
} | ||
selectors = append([]SelectorFunc{selectID}, selectors...) | ||
toDelete, err := ListIdentities(tx, nil, selectors...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another place where I had to move a thing to the selectors. I think this is the only case that was not a Preload
assert.NilError(t, m.Migrate()) | ||
|
||
initialSchema = dumpSchema(t, os.Getenv("POSTGRESQL_CONNECTION")) | ||
|
||
assert.NilError(t, db.Exec("DROP SCHEMA IF EXISTS testing CASCADE").Error) | ||
_, err = db.Exec("DROP SCHEMA IF EXISTS testing CASCADE") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I converted the few remaining Exec
calls to the new interface, which matches the stdlib. So you'll notice the .Error
removed, and that it now returns two args.
return org | ||
} | ||
|
||
func MustGetOrgFromContext(ctx context.Context) *models.Organization { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we have a strongly typed interface we don't need to store the org in the context. This wasn't necessary to do all at once, but it was a very small change at the end (see the last commit).
We no longer panic, but an unset orgID will also be 0, which will never match a real org (not even the default), which will still error and should be caught by a test.
pgsql := postgresDriver(t) | ||
db := setupDB(t, pgsql) | ||
func TestCreateOrganization(t *testing.T) { | ||
runDBTests(t, func(t *testing.T, db *DB) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated this test to use runDBTests
like all the others. We only really need to test against postgres right now, but I expect runDBTests
to be useful in the future despite that.
One use case that comes to mind is testing postgres upgrades. We can run all of our data
tests against multiple versions of postgres to catch problems.
withDBTxn(c.Request.Context(), srv.DB(), func(tx *gorm.DB) { | ||
withDBTxn(c.Request.Context(), srv.DB().GormDB(), func(db *gorm.DB) { | ||
|
||
tx := data.NewTransaction(db, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it was before, we now set the orgID in the transaction as part of these middleware. But until we have an org identified we query with orgID =0, which should ensure we never match an org.
ccc3f42
to
c898a37
Compare
@@ -42,6 +43,10 @@ func TestSignup(t *testing.T) { | |||
assert.Equal(t, identity.Name, user) | |||
assert.Equal(t, identity.OrganizationID, org.ID) | |||
|
|||
// simulate a request |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
return list[models.AccessKey](db, p, selectors...) | ||
} | ||
|
||
func GetAccessKey(db *gorm.DB, selectors ...SelectorFunc) (*models.AccessKey, error) { | ||
func GetAccessKey(tx GormTxn, selectors ...SelectorFunc) (*models.AccessKey, error) { | ||
db := tx.GormDB() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do some queries need tx.GormDB
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anything that builds a query itself using gorm methods will need to call tx.GormDB
. Mostly that is our helpers like add
, save
, get
, but also a few others that do more interesting queries.
This will give us an easy way of finding all the queries that need to be replaced with sql.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found this a little confusing too when reading through the PR. Maybe a note around GormDB()
which explains when to use it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be removing GormDB
very soon, but I'll add a comment for the interim.
} | ||
if err := SaveAccessKey(db, t); err != nil { | ||
|
||
tx = NewTransaction(tx.GormDB(), t.OrganizationID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a nested transaction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming is maybe a bit misleading. NewTransaction
doesn't create a new DB transaction, it uses an existing reference to an sql.Tx
, but sets a new orgID for the transaction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also took me a moment to realize what was going on, and how Exec()
works inside the transaction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed this is done in the wrong place. I'll add a comment and move it to outside of this block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This NewTransaction
wouldn't be necessary if we didn't have the panic
thta was requested in these comments. It's kind of unfortunate that we're forcing unnecessary code because of safeguards that aren't correct in all cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to remove this by moving where we panic:
I'll open that as a follow up
@@ -157,6 +158,7 @@ func unauthenticatedMiddleware(srv *Server) gin.HandlerFunc { | |||
sendAPIError(c, internal.ErrBadRequest) | |||
return | |||
} | |||
authned.Organization = org |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be set directly on the authned
access key already, re-assigning feels dangerous. Or is this just loading the org for the access key's org ID?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the unauthenticated route, so it may not be set yet.
I wasn't sure where to store it. Maybe we should split up the Authenticated
struct into something like this:
type RequestDetails struct {
Authenticated Authenticated
Organization *models.Organization
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like a good idea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like to see the return of MustGetOrg type semantics, where things can't quietly fail when missing. Even in cases where the calling code panics anyway, the panic message is more clear when explicit.
// FIXME: this is a hack to keep our tests passing. The db should not | ||
// be scoped to an org ID. | ||
return d.DefaultOrg.ID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this one. How does this do the right thing in production?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks fine. We still need to somehow split this for privileged vs unprivileged requests, and I'm not 100% sure how to carve that up. We could hold two references inside of the transaction, although it would be preferable only to store the unprivileged connection just in case we mess something up and call something in the data layer which references the wrong connection.
Right now we need the privileged connection to:
- Use the telemetry/metrics
- Load the config (which needs to look up the default org, but we could drop this if we didn't have a default org at all)
- DeleteIdentities() and DeleteGrants() also needs the work somehow in the config which gets rid of any users/grants which were set up previously. This gets really weird if the org changes.
- Encryption keys (since they're not tied to an org -- the unprivileged connection can still look these up right now which is maybe not what we want)
- Migrations
- Sign-up (although this could also work if we don't turn on RLS for the organizations table)
@@ -15,7 +14,7 @@ import ( | |||
"github.com/infrahq/infra/internal/testing/patch" | |||
) | |||
|
|||
func setupDB(t *testing.T) *gorm.DB { | |||
func setupDB(t *testing.T) *data.DB { | |||
t.Helper() | |||
driver := database.PostgresDriver(t, "_authn") | |||
if driver == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be pulling out the SQLite driver now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason to yet. That should be a separate change either way.
return list[models.AccessKey](db, p, selectors...) | ||
} | ||
|
||
func GetAccessKey(db *gorm.DB, selectors ...SelectorFunc) (*models.AccessKey, error) { | ||
func GetAccessKey(tx GormTxn, selectors ...SelectorFunc) (*models.AccessKey, error) { | ||
db := tx.GormDB() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found this a little confusing too when reading through the PR. Maybe a note around GormDB()
which explains when to use it?
} | ||
if err := SaveAccessKey(db, t); err != nil { | ||
|
||
tx = NewTransaction(tx.GormDB(), t.OrganizationID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also took me a moment to realize what was going on, and how Exec()
works inside the transaction.
The When we choose which one to use when we create the transaction. I don't think the changes were should impact that much. The biggest change might be in tests. |
9c59375
to
ee783cd
Compare
I've restored the panic and added more godoc. I don't like the panic, I think it's assuming too much about the caller, which maybe true for regular requests, but is not true for middleware and other callers of the There are some places where we have to create a transaction just to work around the panic. The I've also added some godoc in a few places as requested. Those changes are in the latest 2 commits. This should be ready for another review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me pending any further comments from Steven and Patrick
We will be removing these selectors shortly.
So that all queries fit the same pattern. These special cases did not work with the new GormTxn interface approach.
with the OrgID
We now use the DB transaction to pass the org ID. Also renames CreateOrganization back now that the function doesn't automatically set the OrgID in the context.
ee783cd
to
8245c6b
Compare
Summary
Branched from #3006, that will need to merge first.
This is a rather large PR because it was hard to split this change into smaller pieces. Introducing the interface should be the largest part of the change. After this everything should be easily broken up into much smaller changes.
This PR does the following:
GormTxn
interface that is accepted by all thedata
query functionsThis approach makes our code much safer, because we are no longer defaulting to the default orgID. I'll call out some places where I had to explicitly do this in tests to prevent breaking many tests. I'll go through in a follow up to fix these places.
There are many commits, and a lot of them are very mechanism find/replace. I'll try to call out the most interesting bits with inline comments.
Related to #2415