-
Notifications
You must be signed in to change notification settings - Fork 2
CXH-2380: reach the DB2 native DSN form through connector config #149
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
base: main
Are you sure you want to change the base?
Changes from all commits
a942195
209040e
640c973
dc98939
d03f4c5
66e11c1
a166e4c
4ca63a2
296d680
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -97,6 +97,9 @@ func (c *Connector) Validate(ctx context.Context) (annotations.Annotations, erro | |||||||||||||
|
|
||||||||||||||
| for name, db := range c.dbs { | ||||||||||||||
| if err := db.PingContext(ctx); err != nil { | ||||||||||||||
| if authErr := database.AuthError(err); authErr != nil { | ||||||||||||||
| return nil, authErr | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+100
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: returning
Suggested change
|
||||||||||||||
| return nil, fmt.Errorf("database %q ping failed: %w", name, err) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||||||||||||||||||||||||
| package database | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| "github.com/go-sql-driver/mysql" | ||||||||||||||||||||||||||||||||||||
| "google.golang.org/grpc/codes" | ||||||||||||||||||||||||||||||||||||
| "google.golang.org/grpc/status" | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const mysqlAccessDenied = 1045 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // AuthError returns an Unauthenticated gRPC status when err is a database | ||||||||||||||||||||||||||||||||||||
| // authentication/authorization failure, or nil otherwise. SQLSTATE class 28 | ||||||||||||||||||||||||||||||||||||
| // ("invalid authorization") is the ANSI code drivers report on bad credentials | ||||||||||||||||||||||||||||||||||||
| // (Postgres/Redshift/Vertica/etc. surface it via SQLState()); MySQL is the | ||||||||||||||||||||||||||||||||||||
| // exception, reporting error 1045 with no SQLSTATE. | ||||||||||||||||||||||||||||||||||||
| func AuthError(err error) error { | ||||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| var sqlState interface{ SQLState() string } | ||||||||||||||||||||||||||||||||||||
| if errors.As(err, &sqlState) && strings.HasPrefix(sqlState.SQLState(), "28") { | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: among this repo's vendored drivers, only pgx implements
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
baton-sql/vendor/github.com/ibmdb/go_ibm_db/error.go Lines 29 to 45 in 296d680
|
||||||||||||||||||||||||||||||||||||
| return status.Error(codes.Unauthenticated, "database authentication failed") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| var myErr *mysql.MySQLError | ||||||||||||||||||||||||||||||||||||
| if errors.As(err, &myErr) && myErr.Number == mysqlAccessDenied { | ||||||||||||||||||||||||||||||||||||
| return status.Error(codes.Unauthenticated, "database authentication failed") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package database | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/go-sql-driver/mysql" | ||
| "github.com/jackc/pgx/v5/pgconn" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/status" | ||
| ) | ||
|
|
||
| func TestAuthError(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| err error | ||
| want codes.Code // codes.OK means expect nil | ||
| }{ | ||
| {"nil", nil, codes.OK}, | ||
| {"postgres invalid_password 28P01", &pgconn.PgError{Code: "28P01"}, codes.Unauthenticated}, | ||
| {"postgres invalid_authorization 28000", &pgconn.PgError{Code: "28000"}, codes.Unauthenticated}, | ||
| {"postgres non-auth relation missing 42P01", &pgconn.PgError{Code: "42P01"}, codes.OK}, | ||
| {"postgres auth error wrapped", fmt.Errorf("ping: %w", &pgconn.PgError{Code: "28P01"}), codes.Unauthenticated}, | ||
| {"mysql access denied 1045", &mysql.MySQLError{Number: 1045}, codes.Unauthenticated}, | ||
| {"mysql other 1146", &mysql.MySQLError{Number: 1146}, codes.OK}, | ||
| {"plain error", errors.New("boom"), codes.OK}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := AuthError(tt.err) | ||
| if tt.want == codes.OK { | ||
| if got != nil { | ||
| t.Fatalf("want nil, got %v", got) | ||
| } | ||
| return | ||
| } | ||
| if status.Code(got) != tt.want { | ||
| t.Fatalf("want %v, got %v", tt.want, status.Code(got)) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -361,13 +361,26 @@ func ResolveDatabaseName(opts ConnectOptions) string { | |
| return expanded | ||
| } | ||
| } | ||
| if _, database, isNativeDB2, err := nativeDB2DSN(opts); err == nil && isNativeDB2 { | ||
| return database | ||
| } | ||
| parsedUrl, err := buildConnectionURL(opts) | ||
| if err != nil || parsedUrl == nil { | ||
| return "" | ||
| } | ||
| return strings.TrimPrefix(parsedUrl.Path, "/") | ||
| } | ||
|
|
||
| // hasStructuredConnectFields reports whether opts carries any structured connect | ||
| // field that a native DB2 DSN would make redundant. A native DSN is self-contained; | ||
| // combining it with these (or a per-database override) silently drops them, so the | ||
| // caller rejects the combination. Scheme is excluded: "db2" alongside a native DSN | ||
| // is a supported, explicit hint. | ||
| func hasStructuredConnectFields(opts ConnectOptions) bool { | ||
| return opts.Host != "" || opts.Port != "" || opts.User != "" || | ||
| opts.Password != "" || opts.Database != "" || len(opts.Params) > 0 | ||
| } | ||
|
|
||
| // ConnectMany opens one *sql.DB per name in dbNames. On any per-database failure, | ||
| // every handle opened so far is closed before returning the error. | ||
| func ConnectMany(ctx context.Context, opts ConnectOptions, dbNames []string) (map[string]*sql.DB, DbEngine, error) { | ||
|
|
@@ -404,6 +417,34 @@ func ConnectMany(ctx context.Context, opts ConnectOptions, dbNames []string) (ma | |
| } | ||
|
|
||
| func Connect(ctx context.Context, opts ConnectOptions) (*sql.DB, DbEngine, error) { | ||
| // A native DB2 DSN is an opaque ODBC keyword=value string, not a URL. Routing it | ||
| // through buildConnectionURL corrupts it (url.Parse/.String mangles the opaque form), | ||
| // so hand it to the driver verbatim. See docs/db2.md. | ||
| nativeDSN, _, isNativeDB2, err := nativeDB2DSN(opts) | ||
| if err != nil { | ||
| return nil, Unknown, err | ||
| } | ||
| if isNativeDB2 { | ||
| // A native DSN already carries host, port, credentials, params and the target | ||
| // database. Structured fields or a per-database override (set directly, or by | ||
| // ConnectMany for databases.static / discovery_query) would be silently dropped | ||
| // on the verbatim path, so reject the combination instead of connecting to the | ||
| // wrong database. See docs/db2.md. | ||
| if hasStructuredConnectFields(opts) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: the mutual-exclusion check fires per- |
||
| return nil, Unknown, errors.New( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: this is a pure user-config error, but a bare |
||
| "native DB2 DSN is self-contained and cannot be combined with structured " + | ||
| "connect fields (host, port, user, password, params) or a per-database " + | ||
| "override (connect.database, databases); put every setting in the DSN or " + | ||
| "use the db2:// URL form", | ||
| ) | ||
| } | ||
| db, err := db2.Connect(ctx, nativeDSN) | ||
| if err != nil { | ||
| return nil, Unknown, err | ||
| } | ||
| return db, DB2, nil | ||
| } | ||
|
|
||
| parsedDsn, err := buildConnectionURL(opts) | ||
| if err != nil { | ||
| return nil, Unknown, err | ||
|
|
@@ -468,6 +509,37 @@ func Connect(ctx context.Context, opts ConnectOptions) (*sql.DB, DbEngine, error | |
| } | ||
| } | ||
|
|
||
| // nativeDB2DSN reports whether opts carries a native DB2 DSN: an opaque ODBC | ||
| // keyword=value string (e.g. "HOSTNAME=...;DATABASE=...") rather than a db2:// URL. | ||
| // When it does, the env-expanded string (for verbatim handoff to the driver) and its | ||
| // DATABASE value are returned. The scheme, when set, must be db2; a URL-shaped DSN or | ||
| // foreign scheme is left to the normal URL path. Detection is db2.ParseNativeDSN, shared | ||
| // with convertToDB2DSN's passthrough, so one pass yields both facts without re-splitting. | ||
| func nativeDB2DSN(opts ConnectOptions) (string, string, bool, error) { | ||
| if opts.DSN == "" { | ||
| return "", "", false, nil | ||
| } | ||
| lookup := opts.resolveLookup() | ||
|
|
||
| scheme, err := expandValue(opts.Scheme, lookup) | ||
| if err != nil { | ||
| return "", "", false, err | ||
| } | ||
| if scheme != "" && scheme != "db2" { | ||
| return "", "", false, nil | ||
| } | ||
|
|
||
| dsn, err := expandValue(opts.DSN, lookup) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: |
||
| if err != nil { | ||
| return "", "", false, err | ||
| } | ||
| database, native := db2.ParseNativeDSN(dsn) | ||
| if !native { | ||
| return "", "", false, nil | ||
| } | ||
| return dsn, database, true, nil | ||
| } | ||
|
|
||
| func buildConnectionURL(opts ConnectOptions) (*url.URL, error) { | ||
| var ( | ||
| parsedUrl *url.URL | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.