-
Notifications
You must be signed in to change notification settings - Fork 29
feat: add driver support for experimental host #724
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
Changes from all commits
d0adba1
92e02d5
caf2cf4
a85d1a9
da585ce
58cdbcc
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 |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ package spannerdriver | |
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "crypto/x509" | ||
| "database/sql" | ||
| "database/sql/driver" | ||
| "errors" | ||
|
|
@@ -46,6 +48,7 @@ import ( | |
| "google.golang.org/api/option/internaloption" | ||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/credentials" | ||
| "google.golang.org/grpc/credentials/insecure" | ||
| "google.golang.org/grpc/status" | ||
| "google.golang.org/protobuf/proto" | ||
|
|
@@ -67,6 +70,9 @@ var defaultStatementCacheSize int | |
| // application. | ||
| const LevelNotice = slog.LevelInfo - 1 | ||
|
|
||
| const experimentalHostProject = "default" | ||
| const experimentalHostInstance = "default" | ||
|
|
||
| // Logger that discards everything and skips (almost) all logs. | ||
| var noopLogger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError + 1})) | ||
|
|
||
|
|
@@ -95,7 +101,7 @@ var noopLogger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{L | |
| // - rpcPriority: Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH. | ||
| // | ||
| // Example: `localhost:9010/projects/test-project/instances/test-instance/databases/test-database;usePlainText=true;disableRouteToLeader=true;enableEndToEndTracing=true` | ||
| var dsnRegExp = regexp.MustCompile(`((?P<HOSTGROUP>[\w.-]+(?:\.[\w\.-]+)*[\w\-\._~:/?#\[\]@!\$&'\(\)\*\+,;=.]+)/)?projects/(?P<PROJECTGROUP>(([a-z]|[-.:]|[0-9])+|(DEFAULT_PROJECT_ID)))(/instances/(?P<INSTANCEGROUP>([a-z]|[-]|[0-9])+)(/databases/(?P<DATABASEGROUP>([a-z]|[-]|[_]|[0-9])+))?)?(([\?|;])(?P<PARAMSGROUP>.*))?`) | ||
|
Collaborator
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. The Python wrapper tests are failing, and I think that is related to the changes here. It seems like there are some changes to Also, the regex now has a ^ and $, which would (probably) disallow leading/trailing spaces, which could be the reason.
Contributor
Author
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. Yes the change to HOSTGROUP was intentional. Otherwise it was capturing /databases in the host group itself after projects and instances were made optional As for the error you are kind of correct. I think the spannerlib-python is buggy. The setup was generating the url as localhost:9010projects/test-project/.... But since the ^ and $ was not there the absence of "/" between localhost:9010 and projects didn't raise a error. Now its catching it. I have made the change in the test setup itself. Let me know if you have other suggestions. |
||
| var dsnRegExp = regexp.MustCompile(`^((?P<HOSTGROUP>[\w.-]+(?:\.[\w\.-]+)*[\w\-\._~:#\[\]@!\$&'\(\)\*\+,=.]+)/)?(projects/(?P<PROJECTGROUP>(([a-z]|[-.:]|[0-9])+|(DEFAULT_PROJECT_ID))))?((?:/)?instances/(?P<INSTANCEGROUP>([a-z]|[-]|[0-9])+))?((?:/)?databases/(?P<DATABASEGROUP>([a-z]|[-]|[_]|[0-9])+))?(([\?|;])(?P<PARAMSGROUP>.*))?$`) | ||
|
|
||
| var _ driver.DriverContext = &Driver{} | ||
| var spannerDriver *Driver | ||
|
|
@@ -496,14 +502,34 @@ func ExtractConnectorConfig(dsn string) (ConnectorConfig, error) { | |
| return ConnectorConfig{}, err | ||
| } | ||
|
|
||
| return ConnectorConfig{ | ||
| c := ConnectorConfig{ | ||
| Host: matches["HOSTGROUP"], | ||
| Project: matches["PROJECTGROUP"], | ||
| Instance: matches["INSTANCEGROUP"], | ||
| Database: matches["DATABASEGROUP"], | ||
| Params: params, | ||
| name: dsn, | ||
| }, nil | ||
| } | ||
| if strings.EqualFold(params[propertyIsExperimentalHost.Key()], "true") { | ||
| if c.Host == "" { | ||
| return ConnectorConfig{}, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "host must be specified for experimental host endpoint")) | ||
| } | ||
| c.Configurator = func(config *spanner.ClientConfig, opts *[]option.ClientOption) { | ||
| config.IsExperimentalHost = true | ||
| } | ||
| if matches["INSTANCEGROUP"] == "" { | ||
| c.Instance = experimentalHostInstance | ||
| } | ||
| c.Project = experimentalHostProject | ||
| } else { | ||
| if c.Project == "" { | ||
| return ConnectorConfig{}, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "project must be specified in connection string")) | ||
| } | ||
| if c.Instance == "" { | ||
| return ConnectorConfig{}, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "instance must be specified in connection string")) | ||
| } | ||
| } | ||
| return c, nil | ||
| } | ||
|
|
||
| func extractConnectorParams(paramsString string) (map[string]string, error) { | ||
|
|
@@ -671,6 +697,22 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er | |
| if connectorConfig.Configurator != nil { | ||
| connectorConfig.Configurator(&config, &opts) | ||
| } | ||
| if config.IsExperimentalHost { | ||
| var caCertFile string | ||
| var clientCertFile string | ||
| var clientCertKey string | ||
| assignPropertyValueIfExists(state, propertyCaCertFile, &caCertFile) | ||
| assignPropertyValueIfExists(state, propertyClientCertFile, &clientCertFile) | ||
| assignPropertyValueIfExists(state, propertyClientCertKey, &clientCertKey) | ||
| if caCertFile != "" { | ||
| credOpts, err := createExperimentalHostCredentials(caCertFile, clientCertFile, clientCertKey) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| opts = append(opts, credOpts) | ||
| opts = append(opts, option.WithoutAuthentication()) | ||
| } | ||
| } | ||
| if connectorConfig.AutoConfigEmulator { | ||
| if connectorConfig.Host == "" { | ||
| connectorConfig.Host = "localhost:9010" | ||
|
|
@@ -1656,3 +1698,38 @@ func WithBatchReadOnly(level sql.IsolationLevel) sql.IsolationLevel { | |
| func withBatchReadOnly(level driver.IsolationLevel) driver.IsolationLevel { | ||
| return driver.IsolationLevel(levelBatchReadOnly)<<8 + level | ||
| } | ||
|
|
||
| // createExperimentalHostCredentials is only supported for connecting to experimental | ||
| // hosts. It reads the provided CA certificate file and optionally the | ||
| // client certificate and key files to set up TLS or mutual TLS credentials, and | ||
| // creates gRPC dial options to connect to an experimental host endpoint. | ||
| func createExperimentalHostCredentials(caCertFile, clientCertificateFile, clientCertificateKey string) (option.ClientOption, error) { | ||
| ca, err := os.ReadFile(caCertFile) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read CA certificate file: %w", err) | ||
| } | ||
| capool := x509.NewCertPool() | ||
| if !capool.AppendCertsFromPEM(ca) { | ||
| return nil, fmt.Errorf("failed to append the CA certificate to CA pool") | ||
| } | ||
|
|
||
| if clientCertificateFile != "" && clientCertificateKey != "" { | ||
| // Setting up mutual TLS with both the CA certificate and client certificate. | ||
| cert, err := tls.LoadX509KeyPair(clientCertificateFile, clientCertificateKey) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to load client certificate/key: %w", err) | ||
| } | ||
| creds := credentials.NewTLS(&tls.Config{ | ||
| RootCAs: capool, | ||
| Certificates: []tls.Certificate{cert}, | ||
| }) | ||
| return option.WithGRPCDialOption(grpc.WithTransportCredentials(creds)), nil | ||
| } | ||
| if clientCertificateFile != "" || clientCertificateKey != "" { | ||
| return nil, fmt.Errorf("both client certificate and key must be provided for mTLS, but only one was provided") | ||
| } | ||
|
|
||
| // Setting up TLS with only the CA certificate. | ||
| creds := credentials.NewTLS(&tls.Config{RootCAs: capool}) | ||
| return option.WithGRPCDialOption(grpc.WithTransportCredentials(creds)), nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.