2024-05-09 21:42:06 +00:00
|
|
|
package cloudsql
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
|
|
|
|
|
"cloud.google.com/go/cloudsqlconn"
|
2024-05-31 16:30:56 +00:00
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
2024-05-09 21:42:06 +00:00
|
|
|
"github.com/jackc/pgx/v5/stdlib"
|
|
|
|
|
|
|
|
|
|
"github.com/sourcegraph/sourcegraph/lib/errors"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ConnConfig struct {
|
|
|
|
|
// ConnectionName is the CloudSQL connection name,
|
|
|
|
|
// e.g. '${project}:${region}:${instance}'
|
|
|
|
|
ConnectionName *string
|
|
|
|
|
// User is the Cloud SQL user to connect as, e.g. 'test-sa@test-project.iam'
|
|
|
|
|
User *string
|
|
|
|
|
// Database to connect to.
|
|
|
|
|
Database string
|
|
|
|
|
// DialOptions are any additional options to pass to the underlying
|
|
|
|
|
// cloud-sql-proxy driver.
|
|
|
|
|
DialOptions []cloudsqlconn.DialOption
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open opens a *sql.DB connection to the Cloud SQL instance specified by the
|
|
|
|
|
// ConnConfig.
|
|
|
|
|
//
|
|
|
|
|
// 🔔 If you are connecting to a MSP-provisioned Cloud SQL instance,
|
|
|
|
|
// DO NOT use this - instead, use runtime.Contract.PostgreSQL.OpenDatabase
|
|
|
|
|
// instead.
|
|
|
|
|
func Open(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
cfg ConnConfig,
|
|
|
|
|
) (*sql.DB, error) {
|
|
|
|
|
config, err := getCloudSQLConnConfig(ctx, cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "get CloudSQL connection config")
|
|
|
|
|
}
|
2024-05-31 16:30:56 +00:00
|
|
|
return sql.Open("pgx", stdlib.RegisterConnConfig(config.ConnConfig))
|
2024-05-09 21:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-31 16:30:56 +00:00
|
|
|
// GetConnectionPool is an alternative to OpenDatabase that returns a
|
|
|
|
|
// github.com/jackc/pgx/v5/pgxpool to the CloudSQL instance specified by
|
|
|
|
|
// the ConnConfig, for services that prefer to use 'pgx' directly. A pool returns
|
|
|
|
|
// without waiting for any connections to be established. Acquire a connection
|
|
|
|
|
// immediately after creating the pool to check if a connection can successfully
|
|
|
|
|
// be established.
|
2024-05-09 21:42:06 +00:00
|
|
|
//
|
|
|
|
|
// 🔔 If you are connecting to a MSP-provisioned Cloud SQL instance,
|
2024-05-31 16:30:56 +00:00
|
|
|
// DO NOT use this - instead, use runtime.Contract.PostgreSQL.GetConnectionPool
|
2024-05-09 21:42:06 +00:00
|
|
|
// instead.
|
2024-05-31 16:30:56 +00:00
|
|
|
func GetConnectionPool(
|
2024-05-09 21:42:06 +00:00
|
|
|
ctx context.Context,
|
|
|
|
|
cfg ConnConfig,
|
2024-05-31 16:30:56 +00:00
|
|
|
) (*pgxpool.Pool, error) {
|
2024-05-09 21:42:06 +00:00
|
|
|
config, err := getCloudSQLConnConfig(ctx, cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "get CloudSQL connection config")
|
|
|
|
|
}
|
2024-05-31 16:30:56 +00:00
|
|
|
return pgxpool.NewWithConfig(ctx, config)
|
2024-05-09 21:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getCloudSQLConnConfig generates a pgx connection configuration for using
|
|
|
|
|
// a Cloud SQL instance using IAM auth.
|
|
|
|
|
func getCloudSQLConnConfig(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
cfg ConnConfig,
|
2024-05-31 16:30:56 +00:00
|
|
|
) (*pgxpool.Config, error) {
|
2024-05-09 21:42:06 +00:00
|
|
|
if cfg.ConnectionName == nil || cfg.User == nil {
|
|
|
|
|
return nil, errors.New("missing required PostgreSQL configuration")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://github.com/GoogleCloudPlatform/cloud-sql-go-connector?tab=readme-ov-file#automatic-iam-database-authentication
|
|
|
|
|
dsn := fmt.Sprintf("user=%s dbname=%s", *cfg.User, cfg.Database)
|
2024-05-31 16:30:56 +00:00
|
|
|
config, err := pgxpool.ParseConfig(dsn)
|
2024-05-09 21:42:06 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "pgx.ParseConfig")
|
|
|
|
|
}
|
|
|
|
|
customDialer, err := cloudsqlconn.NewDialer(ctx,
|
|
|
|
|
// always the case when using Cloud SQL in MSP
|
|
|
|
|
cloudsqlconn.WithIAMAuthN(),
|
|
|
|
|
// allow passthrough of additional dial options
|
|
|
|
|
cloudsqlconn.WithDefaultDialOptions(cfg.DialOptions...))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "cloudsqlconn.NewDialer")
|
|
|
|
|
}
|
|
|
|
|
// Use the Cloud SQL connector to handle connecting to the instance.
|
|
|
|
|
// This approach does *NOT* require the Cloud SQL proxy.
|
2024-05-31 16:30:56 +00:00
|
|
|
config.ConnConfig.DialFunc = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
2024-05-09 21:42:06 +00:00
|
|
|
return customDialer.Dial(ctx, *cfg.ConnectionName)
|
|
|
|
|
}
|
feat/enterprise-portal: DB layer for {Get/List}CodyGatewayAccess (#62706)
Part of CORE-112. We need to implement the `CodyAccess` service proposed in https://github.com/sourcegraph/sourcegraph/pull/62263, so that Cody Gateway can depend on it as we start a transition over to Enterprise Portal as the source-or-truth for Cody Gateway access; see the [Linear project](https://linear.app/sourcegraph/project/kr-launch-enterprise-portal-for-cody-gateway-and-cody-analytics-ee5d9ea105c2/overview). This PR implements the data layer by reading directly from the Sourcegraph.com Cloud SQL database, and a subsequent PR https://github.com/sourcegraph/sourcegraph/pull/62771 will expose this via the API and also implement auth; nothing in this PR is used yet.
Most things in this PR will be undone by the end of a [follow-up project](https://linear.app/sourcegraph/project/kr-enterprise-portal-manages-all-enterprise-subscriptions-12f1d5047bd2/overview) tentatively slated for completion by end-of-August.
### Query
I've opted to write a new query specifically to fetch the data required to fulfill the proposed `CodyAccess` RPCs; the existing queries fetch a lot more than is strictly needed, and often make multiple round trips to the database. The new query fetches everything it needs for get/list in a single round trip.
`EXPLAIN ANALYZE` of the new list-all query against the Sourcegraph.com production database indicates this is likely performant enough for our internal-only use cases, especially as this will only be around for a few months.
```
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=1610.56..1629.45 rows=1511 width=121) (actual time=23.358..24.921 rows=1512 loops=1)
Group Key: ps.id
-> Hash Left Join (cost=958.18..1585.58 rows=1999 width=1094) (actual time=8.258..12.255 rows=2748 loops=1)
Hash Cond: (ps.id = active_license.product_subscription_id)
-> Hash Right Join (cost=67.00..689.14 rows=1999 width=956) (actual time=1.098..3.970 rows=2748 loops=1)
Hash Cond: (product_licenses.product_subscription_id = ps.id)
-> Seq Scan on product_licenses (cost=0.00..616.88 rows=1999 width=919) (actual time=0.015..1.769 rows=2002 loops=1)
Filter: (access_token_enabled IS TRUE)
Rows Removed by Filter: 1789
-> Hash (cost=48.11..48.11 rows=1511 width=53) (actual time=1.055..1.056 rows=1512 loops=1)
Buckets: 2048 Batches: 1 Memory Usage: 93kB
-> Seq Scan on product_subscriptions ps (cost=0.00..48.11 rows=1511 width=53) (actual time=0.016..0.552 rows=1512 loops=1)
-> Hash (cost=874.39..874.39 rows=1343 width=154) (actual time=7.123..7.125 rows=1343 loops=1)
Buckets: 2048 Batches: 1 Memory Usage: 248kB
-> Subquery Scan on active_license (cost=842.02..874.39 rows=1343 width=154) (actual time=5.425..6.461 rows=1343 loops=1)
-> Unique (cost=842.02..860.96 rows=1343 width=162) (actual time=5.422..6.268 rows=1343 loops=1)
-> Sort (cost=842.02..851.49 rows=3788 width=162) (actual time=5.421..5.719 rows=3791 loops=1)
Sort Key: product_licenses_1.product_subscription_id, product_licenses_1.created_at DESC
Sort Method: quicksort Memory: 1059kB
-> Seq Scan on product_licenses product_licenses_1 (cost=0.00..616.88 rows=3788 width=162) (actual time=0.003..1.872 rows=3791 loops=1)
Planning Time: 2.266 ms
Execution Time: 28.568 ms
```
We noted the lack of index on `product_livenses.subscription_id`, but it doesn't seem to be an issue at this scale, so I've left it as is.
### Pagination
After discussing with Erik, we decided there is no need to implement pagination for the list-all RPC yet; a rough upper bound of 1kb per subscription * 1511 rows (see `EXPLAIN ANALYZE` above) is 1.5MB, which is well below the per-message limits we have set for Sourcegraph-internal traffic (40MB), and below the [default 4MB limit](https://pkg.go.dev/google.golang.org/grpc#MaxRecvMsgSize) as well. In https://github.com/sourcegraph/sourcegraph/pull/62771 providing pagination parameters will result in a `CodeUnimplemented` error.
We can figure out how we want to implement pagination as part of the [follow-up project](https://linear.app/sourcegraph/project/kr-enterprise-portal-manages-all-enterprise-subscriptions-12f1d5047bd2/overview) to migrate the data to an Enterprise-Portal-owned database.
### Testing
A good chunk of this PR's changes are exposing a small set of `cmd/frontend` internals **for testing** via the new `cmd/frontend/dotcomproductsubscriptiontest`:
- seeding test databases with subscriptions and licenses
- for "regression testing" the new read queries by validating what the new read queries get, against what the existing GraphQL resolvers resolve to. This is important because the GraphQL resolvers has a lot of the override logic
See `TestGetCodyGatewayAccessAttributes` for how all this is used.
<img width="799" alt="image" src="https://github.com/sourcegraph/sourcegraph/assets/23356519/af4d0c1e-c9a9-448a-9b8e-0f328688a75a">
There is also some hackery involved in setting up a `pgx/v5` connection used in MSP from the `sql.DB` + `pgx/v4` stuff used by `dbtest`; see `newTestDotcomReader` docstrings for details.
## Test plan
```
go test -v ./cmd/enterprise-portal/internal/dotcomdb
```
---
Co-authored-by: Chris Smith <chrsmith@users.noreply.github.com>
Co-authored-by: Joe Chen <joe@sourcegraph.com>
2024-05-22 19:56:59 +00:00
|
|
|
// Attach tracing
|
2024-05-31 16:30:56 +00:00
|
|
|
config.ConnConfig.Tracer = pgxTracer{}
|
feat/enterprise-portal: DB layer for {Get/List}CodyGatewayAccess (#62706)
Part of CORE-112. We need to implement the `CodyAccess` service proposed in https://github.com/sourcegraph/sourcegraph/pull/62263, so that Cody Gateway can depend on it as we start a transition over to Enterprise Portal as the source-or-truth for Cody Gateway access; see the [Linear project](https://linear.app/sourcegraph/project/kr-launch-enterprise-portal-for-cody-gateway-and-cody-analytics-ee5d9ea105c2/overview). This PR implements the data layer by reading directly from the Sourcegraph.com Cloud SQL database, and a subsequent PR https://github.com/sourcegraph/sourcegraph/pull/62771 will expose this via the API and also implement auth; nothing in this PR is used yet.
Most things in this PR will be undone by the end of a [follow-up project](https://linear.app/sourcegraph/project/kr-enterprise-portal-manages-all-enterprise-subscriptions-12f1d5047bd2/overview) tentatively slated for completion by end-of-August.
### Query
I've opted to write a new query specifically to fetch the data required to fulfill the proposed `CodyAccess` RPCs; the existing queries fetch a lot more than is strictly needed, and often make multiple round trips to the database. The new query fetches everything it needs for get/list in a single round trip.
`EXPLAIN ANALYZE` of the new list-all query against the Sourcegraph.com production database indicates this is likely performant enough for our internal-only use cases, especially as this will only be around for a few months.
```
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=1610.56..1629.45 rows=1511 width=121) (actual time=23.358..24.921 rows=1512 loops=1)
Group Key: ps.id
-> Hash Left Join (cost=958.18..1585.58 rows=1999 width=1094) (actual time=8.258..12.255 rows=2748 loops=1)
Hash Cond: (ps.id = active_license.product_subscription_id)
-> Hash Right Join (cost=67.00..689.14 rows=1999 width=956) (actual time=1.098..3.970 rows=2748 loops=1)
Hash Cond: (product_licenses.product_subscription_id = ps.id)
-> Seq Scan on product_licenses (cost=0.00..616.88 rows=1999 width=919) (actual time=0.015..1.769 rows=2002 loops=1)
Filter: (access_token_enabled IS TRUE)
Rows Removed by Filter: 1789
-> Hash (cost=48.11..48.11 rows=1511 width=53) (actual time=1.055..1.056 rows=1512 loops=1)
Buckets: 2048 Batches: 1 Memory Usage: 93kB
-> Seq Scan on product_subscriptions ps (cost=0.00..48.11 rows=1511 width=53) (actual time=0.016..0.552 rows=1512 loops=1)
-> Hash (cost=874.39..874.39 rows=1343 width=154) (actual time=7.123..7.125 rows=1343 loops=1)
Buckets: 2048 Batches: 1 Memory Usage: 248kB
-> Subquery Scan on active_license (cost=842.02..874.39 rows=1343 width=154) (actual time=5.425..6.461 rows=1343 loops=1)
-> Unique (cost=842.02..860.96 rows=1343 width=162) (actual time=5.422..6.268 rows=1343 loops=1)
-> Sort (cost=842.02..851.49 rows=3788 width=162) (actual time=5.421..5.719 rows=3791 loops=1)
Sort Key: product_licenses_1.product_subscription_id, product_licenses_1.created_at DESC
Sort Method: quicksort Memory: 1059kB
-> Seq Scan on product_licenses product_licenses_1 (cost=0.00..616.88 rows=3788 width=162) (actual time=0.003..1.872 rows=3791 loops=1)
Planning Time: 2.266 ms
Execution Time: 28.568 ms
```
We noted the lack of index on `product_livenses.subscription_id`, but it doesn't seem to be an issue at this scale, so I've left it as is.
### Pagination
After discussing with Erik, we decided there is no need to implement pagination for the list-all RPC yet; a rough upper bound of 1kb per subscription * 1511 rows (see `EXPLAIN ANALYZE` above) is 1.5MB, which is well below the per-message limits we have set for Sourcegraph-internal traffic (40MB), and below the [default 4MB limit](https://pkg.go.dev/google.golang.org/grpc#MaxRecvMsgSize) as well. In https://github.com/sourcegraph/sourcegraph/pull/62771 providing pagination parameters will result in a `CodeUnimplemented` error.
We can figure out how we want to implement pagination as part of the [follow-up project](https://linear.app/sourcegraph/project/kr-enterprise-portal-manages-all-enterprise-subscriptions-12f1d5047bd2/overview) to migrate the data to an Enterprise-Portal-owned database.
### Testing
A good chunk of this PR's changes are exposing a small set of `cmd/frontend` internals **for testing** via the new `cmd/frontend/dotcomproductsubscriptiontest`:
- seeding test databases with subscriptions and licenses
- for "regression testing" the new read queries by validating what the new read queries get, against what the existing GraphQL resolvers resolve to. This is important because the GraphQL resolvers has a lot of the override logic
See `TestGetCodyGatewayAccessAttributes` for how all this is used.
<img width="799" alt="image" src="https://github.com/sourcegraph/sourcegraph/assets/23356519/af4d0c1e-c9a9-448a-9b8e-0f328688a75a">
There is also some hackery involved in setting up a `pgx/v5` connection used in MSP from the `sql.DB` + `pgx/v4` stuff used by `dbtest`; see `newTestDotcomReader` docstrings for details.
## Test plan
```
go test -v ./cmd/enterprise-portal/internal/dotcomdb
```
---
Co-authored-by: Chris Smith <chrsmith@users.noreply.github.com>
Co-authored-by: Joe Chen <joe@sourcegraph.com>
2024-05-22 19:56:59 +00:00
|
|
|
|
2024-05-09 21:42:06 +00:00
|
|
|
return config, nil
|
|
|
|
|
}
|