-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat/enterpriseportal: db layer for subscription licenses #63792
Conversation
53fdf23
to
dc7fed6
Compare
_ pgx.QueryTracer = pgxTestTracer{} | ||
) | ||
|
||
func (t pgxTestTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context { |
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.
Handy for dumping DB queries in tests
// Time is a wrapper around time.Time that implements the database/sql.Scanner | ||
// and database/sql/driver.Valuer interfaces to serialize and deserialize time | ||
// in UTC time zone. | ||
type Time time.Time |
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.
Primarily avoids scanning times in anything but UTC for the API, but for consistency can also be used to make sure we only ever feed UTC times into the DB
)`, pgx.NamedArgs{ | ||
"licenseID": licenseID, | ||
// Convert to string representation of EnterpriseSubscriptionLicenseCondition | ||
"status": subscriptionsv1.EnterpriseSubscriptionLicenseCondition_Status_name[int32(opts.Status)], |
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 "shouldn't" be using API types in storage, but limited sets of enums are okay I think?
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.
Probably - but it would probably also be easy to write a small function that maps status to DB value manually in a switch 😬
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.
Right now the values are pretty simple, like STATUS_REVOKED
, STATUS_CREATED
, etc, which is pretty much what I would put in the DB right now.
I think if we get to a point where the API enums start to deviate from the DB, we can then introduce a mapper for the values already in there
func (s *TableSubscriptionLicense) RunCustomMigrations(migrator gorm.Migrator) error { | ||
if migrator.HasColumn(s, "license_kind") { | ||
if err := migrator.DropColumn(s, "license_kind"); err != nil { | ||
return err | ||
} | ||
} | ||
return 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.
Drop renamed columns when gorm refuses to. This is not in use yet so is safe to do
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 migration process seems to be quite painful :/
side-note: would be cool if we had the same schema.md
generator for this DB so it's easier to diff what gorm does across commits 😬
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 love to have that, though at a cursory search I haven't found a good tool that does this for the gorm schema defs 😬 Maybe we can do something similar to how schema.md
is generated, where we apply the schema to a DB and then introspect it with some tool to generate the page
https://linear.app/sourcegraph/issue/CORE-221/enterpriseportal-database-schema-doc-generator
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.
Have you covered https://github.com/gogs/gogs/blob/main/internal/database/schemadoc/main.go 😂 Output https://github.com/gogs/gogs/blob/main/docs/dev/database_schema.md
Also although not recommending (we should migrate to a better migration system), but GORM-based migrations prior art 😂 : https://github.com/gogs/gogs/tree/main/internal/database/migrations
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.
Thanks, added to the issue!
// LicenseData is the license data stored in JSON format. It is read-only | ||
// and generally never queried in conditions - properties that are should | ||
// be stored at the subscription or license level. | ||
// | ||
// Value shapes correspond to API types appropriate for each | ||
// 'EnterpriseSubscriptionLicenseType'. | ||
LicenseData pgtype.JSONB `gorm:"type:jsonb"` | ||
LicenseData json.RawMessage `gorm:"type:jsonb"` |
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.
Allow different types of licenses to be stored in the same table, persisted as jsonb for query, though most use cases will not require querying over jsonb
// LicenseKey corresponds to *subscriptionsv1.EnterpriseSubscriptionLicenseKey | ||
// and the 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' license type. | ||
type LicenseKey struct { | ||
Info internallicense.Info |
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.
Use internal type in storage layer for consistency
dc7fed6
to
0ee398b
Compare
@eseliger for optional review if you have time 🙏 |
Actually need a few more fixes, brb |
192a825
to
ebe188c
Compare
@@ -33,27 +50,23 @@ type Subscription struct { | |||
// | |||
// TODO: Clean up the database post-deploy and remove the 'Unnamed subscription' | |||
// part of the constraint. | |||
DisplayName string `gorm:"size:256;not null;uniqueIndex:,where:archived_at IS NULL AND display_name != 'Unnamed subscription' AND display_name != ''"` | |||
DisplayName *string `gorm:"size:256;uniqueIndex:,where:archived_at IS NULL AND display_name != 'Unnamed subscription' AND display_name != ''"` |
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.
InstanceDomain is nullable, this change makes it explicit
DisplayName is required at API level, but realistically our old data won't have it. Rather than special treatment for empty values or stub values nullable is easier
ebe188c
to
6a9d351
Compare
785cf80
to
0589692
Compare
@@ -83,17 +90,20 @@ func maybeMigrate(ctx context.Context, logger log.Logger, contract runtime.Contr | |||
span.AddEvent("lock.acquired") | |||
|
|||
versionKey := fmt.Sprintf("%s:db_version", dbName) | |||
liveVersion := redisClient.Get(ctx, versionKey).Val() |
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.
it seems a bit odd to me that redis would keep the state of the SQL DB :D
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.
Hm, we should probably store it in DB huh
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.
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.
Start storing this info in DB somewhat increases the seriousness of the value, that requires us to keep it legit and deal with potential compatibility issues. The current use case of it (and why it's in Redis) is merely a quick optimization about "avoid (if possible) running DB migrations if the version is exactly the same, and it's fine if ran twice, does no harm", it doesn't really care if an upgrade or downgrade has happened.
)`, pgx.NamedArgs{ | ||
"licenseID": licenseID, | ||
// Convert to string representation of EnterpriseSubscriptionLicenseCondition | ||
"status": subscriptionsv1.EnterpriseSubscriptionLicenseCondition_Status_name[int32(opts.Status)], |
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.
Probably - but it would probably also be easy to write a small function that maps status to DB value manually in a switch 😬
func (s *TableSubscriptionLicense) RunCustomMigrations(migrator gorm.Migrator) error { | ||
if migrator.HasColumn(s, "license_kind") { | ||
if err := migrator.DropColumn(s, "license_kind"); err != nil { | ||
return err | ||
} | ||
} | ||
return 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.
the migration process seems to be quite painful :/
side-note: would be cool if we had the same schema.md
generator for this DB so it's easier to diff what gorm does across commits 😬
Implements Cody Gateway access in Enterprise Portal DB, such that it replicates the behaviour it has today, retrieving: - Quota overrides - Subscription display name - Active license info - Non-revoked, non-expired license keys as hashes - Revocation + non-expiry replaces the existing mechanism of flagging licenses as `access_token_enabled`. Since we ended up doing zero-config for Cody Gateway, the only license hashes that are valid for Cody Gateway are non-expired licenses - once your license expires you should be switching to a new license key anyway. It's fairly similar to the `dotcomdb` shim we built before, but for our new tables. See https://github.com/sourcegraph/sourcegraph/pull/63792 for the licenses tables. None of this is going live yet. Part of https://linear.app/sourcegraph/issue/CORE-100 Part of https://linear.app/sourcegraph/issue/CORE-160 ## Test plan DB integration tests `sg run enterprise-portal` does the migrations without a hitch
Implements insert and retrieval of subscription conditions, similar to #63792 Unlike for licenses, which have a more limited lifecycle (create and revoke), subscriptions are longer-lived and may be updated frequently. So the storage layer allows the caller to provide the conditions that are of interest for recording. Part of https://linear.app/sourcegraph/issue/CORE-156, but doesn't yet use it from the API. Part of https://linear.app/sourcegraph/issue/CORE-100 ## Test plan Integration tests
Implements CRUD on the new licenses DB. I had to make significant changes from the initial setup after spending more time working on this.
There's lots of schema changes but that's okay, as we have no data yet.
As in the RPC design, this is intended to accommodate new "types" of licensing in the future, and so the DB is structured as such as well. There's also feedback that context around license management events is very useful - this is encoded in the conditions table, and can be extended to include more types of conditions in the future.
Part of https://linear.app/sourcegraph/issue/CORE-158
Part of https://linear.app/sourcegraph/issue/CORE-100
Test plan
Integration tests
Locally, running
sg run enterprise-portal
indicates migrations proceed as expected