options.go (185 lines of code) (raw):
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alloydbconn
import (
"context"
"crypto/rsa"
"io"
"net"
"net/http"
"os"
"time"
"cloud.google.com/go/alloydbconn/debug"
"cloud.google.com/go/alloydbconn/errtype"
"cloud.google.com/go/alloydbconn/internal/alloydb"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
apiopt "google.golang.org/api/option"
)
// CloudPlatformScope is the default OAuth2 scope set on the API client.
const CloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
// An Option is an option for configuring a Dialer.
type Option func(d *dialerConfig)
type dialerConfig struct {
rsaKey *rsa.PrivateKey
// alloydbClientOpts are options to configure only the AlloyDB Rest API
// client. Configuration that should apply to all Google Cloud API clients
// should be included in clientOpts.
alloydbClientOpts []apiopt.ClientOption
// clientOpts are options to configure any Google Cloud API client. They
// should not include any AlloyDB-specific configuration.
clientOpts []apiopt.ClientOption
dialOpts []DialOption
dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
refreshTimeout time.Duration
tokenSource oauth2.TokenSource
userAgents []string
useIAMAuthN bool
logger debug.ContextLogger
lazyRefresh bool
// disableMetadataExchange is a temporary addition and will be removed in
// future versions.
disableMetadataExchange bool
// disableBuiltInTelemetry disables the internal metric exporter.
disableBuiltInTelemetry bool
staticConnInfo io.Reader
// err tracks any dialer options that may have failed.
err error
}
// WithOptions turns a list of Option's into a single Option.
func WithOptions(opts ...Option) Option {
return func(d *dialerConfig) {
for _, opt := range opts {
opt(d)
}
}
}
// WithCredentialsFile returns an Option that specifies a service account
// or refresh token JSON credentials file to be used as the basis for
// authentication.
func WithCredentialsFile(filename string) Option {
return func(d *dialerConfig) {
b, err := os.ReadFile(filename)
if err != nil {
d.err = errtype.NewConfigError(err.Error(), "n/a")
return
}
opt := WithCredentialsJSON(b)
opt(d)
}
}
// WithCredentialsJSON returns an Option that specifies a service account
// or refresh token JSON credentials to be used as the basis for authentication.
func WithCredentialsJSON(b []byte) Option {
return func(d *dialerConfig) {
// TODO: Use AlloyDB-specfic scope
c, err := google.CredentialsFromJSON(context.Background(), b, CloudPlatformScope)
if err != nil {
d.err = errtype.NewConfigError(err.Error(), "n/a")
return
}
d.tokenSource = c.TokenSource
d.clientOpts = append(d.clientOpts, apiopt.WithCredentials(c))
}
}
// WithUserAgent returns an Option that sets the User-Agent.
func WithUserAgent(ua string) Option {
return func(d *dialerConfig) {
d.userAgents = append(d.userAgents, ua)
}
}
// WithDefaultDialOptions returns an Option that specifies the default
// DialOptions used.
func WithDefaultDialOptions(opts ...DialOption) Option {
return func(d *dialerConfig) {
d.dialOpts = append(d.dialOpts, opts...)
}
}
// WithTokenSource returns an Option that specifies an OAuth2 token source
// to be used as the basis for authentication.
func WithTokenSource(s oauth2.TokenSource) Option {
return func(d *dialerConfig) {
d.tokenSource = s
d.clientOpts = append(d.clientOpts, apiopt.WithTokenSource(s))
}
}
// WithRSAKey returns an Option that specifies a rsa.PrivateKey used to
// represent the client.
func WithRSAKey(k *rsa.PrivateKey) Option {
return func(d *dialerConfig) {
d.rsaKey = k
}
}
// WithRefreshTimeout returns an Option that sets a timeout on refresh
// operations. Defaults to 60s.
func WithRefreshTimeout(t time.Duration) Option {
return func(d *dialerConfig) {
d.refreshTimeout = t
}
}
// WithHTTPClient configures the underlying AlloyDB Admin API client with the
// provided HTTP client. This option is generally unnecessary except for
// advanced use-cases.
func WithHTTPClient(client *http.Client) Option {
return func(d *dialerConfig) {
d.clientOpts = append(d.clientOpts, apiopt.WithHTTPClient(client))
}
}
// WithAdminAPIEndpoint configures the underlying AlloyDB Admin API client to
// use the provided URL.
func WithAdminAPIEndpoint(url string) Option {
return func(d *dialerConfig) {
d.alloydbClientOpts = append(d.alloydbClientOpts, apiopt.WithEndpoint(url))
}
}
// WithDialFunc configures the function used to connect to the address on the
// named network. This option is generally unnecessary except for advanced
// use-cases. The function is used for all invocations of Dial. To configure
// a dial function per individual calls to dial, use WithOneOffDialFunc.
func WithDialFunc(dial func(ctx context.Context, network, addr string) (net.Conn, error)) Option {
return func(d *dialerConfig) {
d.dialFunc = dial
}
}
// WithIAMAuthN enables automatic IAM Authentication. If no token source has
// been configured (such as with WithTokenSource, WithCredentialsFile, etc),
// the dialer will use the default token source as defined by
// https://pkg.go.dev/golang.org/x/oauth2/google#FindDefaultCredentialsWithParams.
func WithIAMAuthN() Option {
return func(d *dialerConfig) {
d.useIAMAuthN = true
}
}
type debugLoggerWithoutContext struct {
logger debug.Logger
}
// Debugf implements debug.ContextLogger.
func (d *debugLoggerWithoutContext) Debugf(_ context.Context, format string, args ...any) {
d.logger.Debugf(format, args...)
}
var _ debug.ContextLogger = new(debugLoggerWithoutContext)
// WithDebugLogger configures a debug logger for reporting on internal
// operations. By default the debug logger is disabled.
// Prefer WithContextLogger.
func WithDebugLogger(l debug.Logger) Option {
return func(d *dialerConfig) {
d.logger = &debugLoggerWithoutContext{l}
}
}
// WithContextLogger configures a debug lgoger for reporting on internal
// operations. By default the debug logger is disabled.
func WithContextLogger(l debug.ContextLogger) Option {
return func(d *dialerConfig) {
d.logger = l
}
}
// WithLazyRefresh configures the dialer to refresh certificates on an
// as-needed basis. If a certificate is expired when a connection request
// occurs, the Go Connector will block the attempt and refresh the certificate
// immediately. This option is useful when running the Go Connector in
// environments where the CPU may be throttled, thus preventing a background
// goroutine from running consistently (e.g., in Cloud Run the CPU is throttled
// outside of a request context causing the background refresh to fail).
func WithLazyRefresh() Option {
return func(d *dialerConfig) {
d.lazyRefresh = true
}
}
// WithStaticConnectionInfo specifies an io.Reader from which to read static
// connection info. This is a *dev-only* option and should not be used in
// production as it will result in failed connections after the client
// certificate expires. It is also subject to breaking changes in the format.
// NOTE: The static connection info is not refreshed by the dialer. The JSON
// format supports multiple instances, regardless of cluster.
//
// The reader should hold JSON with the following format:
//
// {
// "publicKey": "<PEM Encoded public RSA key>",
// "privateKey": "<PEM Encoded private RSA key>",
// "projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>": {
// "ipAddress": "<PSA-based private IP address>",
// "publicIpAddress": "<public IP address>",
// "pscInstanceConfig": {
// "pscDnsName": "<PSC DNS name>"
// },
// "pemCertificateChain": [
// "<client cert>", "<intermediate cert>", "<CA cert>"
// ],
// "caCert": "<CA cert>"
// }
// }
func WithStaticConnectionInfo(r io.Reader) Option {
return func(d *dialerConfig) {
d.staticConnInfo = r
}
}
// WithOptOutOfAdvancedConnectionCheck disables the dataplane permission check.
// It is intended only for clients who are running in an environment where the
// workload's IP address is otherwise unknown and cannot be allow-listed in a
// VPC Service Control security perimeter. This option is incompatible with IAM
// Authentication.
//
// NOTE: This option is for internal usage only and is meant to ease the
// migration when the advanced check will be required on the server. In future
// versions this will revert to a no-op and should not be used. If you think
// you need this option, open an issue on
// https://github.com/GoogleCloudPlatform/alloydb-go-connector for design
// advice.
func WithOptOutOfAdvancedConnectionCheck() Option {
return func(d *dialerConfig) {
d.disableMetadataExchange = true
}
}
// WithOptOutOfBuiltInTelemetry disables the internal metric export. By
// default, the Dialer will report on its internal operations to the
// alloydb.googleapis.com system metric prefix. These metrics help AlloyDB
// improve performance and identify client connectivity problems. Presently,
// these metrics aren't public, but will be made public in the future. To
// disable this telemetry, provide this option when initializing a Dialer.
func WithOptOutOfBuiltInTelemetry() Option {
return func(d *dialerConfig) {
d.disableBuiltInTelemetry = true
}
}
// A DialOption is an option for configuring how a Dialer's Dial call is
// executed.
type DialOption func(d *dialCfg)
type dialCfg struct {
dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
ipType string
tcpKeepAlive time.Duration
}
// DialOptions turns a list of DialOption instances into an DialOption.
func DialOptions(opts ...DialOption) DialOption {
return func(cfg *dialCfg) {
for _, opt := range opts {
opt(cfg)
}
}
}
// WithOneOffDialFunc configures the dial function on a one-off basis for an
// individual call to Dial. To configure a dial function across all invocations
// of Dial, use WithDialFunc.
func WithOneOffDialFunc(dial func(ctx context.Context, network, addr string) (net.Conn, error)) DialOption {
return func(c *dialCfg) {
c.dialFunc = dial
}
}
// WithTCPKeepAlive returns a DialOption that specifies the tcp keep alive
// period for the connection returned by Dial.
func WithTCPKeepAlive(d time.Duration) DialOption {
return func(cfg *dialCfg) {
cfg.tcpKeepAlive = d
}
}
// WithPublicIP returns a DialOption that specifies a public IP will be used to
// connect.
func WithPublicIP() DialOption {
return func(cfg *dialCfg) {
cfg.ipType = alloydb.PublicIP
}
}
// WithPrivateIP returns a DialOption that specifies a private IP (VPC) will be
// used to connect.
func WithPrivateIP() DialOption {
return func(cfg *dialCfg) {
cfg.ipType = alloydb.PrivateIP
}
}
// WithPSC returns a DialOption that specifies a PSC endpoint will be used to
// connect.
func WithPSC() DialOption {
return func(cfg *dialCfg) {
cfg.ipType = alloydb.PSC
}
}