lazydsn

package module
v0.0.0-...-b8903f5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 5, 2019 License: MIT Imports: 3 Imported by: 0

README

Lazy DSN database driver for Go

GoDoc Go Report Card

This package implements a database agnostic, database/sql style driver, with delayed DSN evaluation. It's a wrapper for a real driver but, instead of using the DSN as provided by Go's package, it relies on a provider to resolve the DSN with every new connection attempt. This allows for use cases where, despite connecting to the same (or functionally equivalent) database, applications are forced to cope with ever rotating credentials, where the password or even also the user change with time. Credentials rotation is typical in highly secured environments and needs to be performed online, while applications are running. This fully supports services such as AWS Secrets Manager.

A "simple" approach to cope with rotating credentials is detecting connection errors and reopening the database. However, this can get tricky pretty fast. Go's database/sql package keeps a pool of connections, recycling them as needed on failures, timeout, etc. This means that connections can be attempted at anytime, prompting the need to add instrumentation everywhere. This, in turn, complicates the code, is error prone and more difficult to maintain.

This package proposes a different approach, moving instrumentation to the driver side instead. By doing this, every new connection that is required in the pool will be opened with updated credentials. If you use regular rotation, you only need to configure your connections to have a bounded lifetime. See sql.SetConnMaxLifetime. Your application may look like this:

import (
	"database/sql"
	"time"

	"github.com/gkristic/lazydsn"
	"github.com/go-sql-driver/mysql"
)

const alias = "lazydsn:mysql"

func main() {
	lazydsn.Register(alias, &mysql.MySQLDriver{},
		lazydsn.DSNProviderFunc(func (dsn string) (string, error) {
			// Compute a new dsn; e.g., by using AWS Secrets Manager
			return dsn, nil
		}),
	)

	db := sql.Open(alias, "arn:...")
	db.SetConnMaxLifetime(time.Hour)
	// Keep working with db as usual
}

Documentation

Overview

Package lazydsn implements a database agnostic driver with delayed DSN evaluation. This allows for use cases where, despite connecting to the same (or functionally equivalent) database, applications are forced to cope with ever rotating credentials, where the password or even also the user change with time. Credentials rotation is typical in highly secured environments and needs to be performed online, while applications are running. This fully supports services such as AWS Secrets Manager.

Warning: Please make sure that you do NOT fundamentally change access when you rotate credentials. Changing the password is fine. Switching to a different user works, as long as both users have exactly equivalent access privileges (grants and whatever applies to your database). The database/sql package assumes a constant DSN after the database is opened. If access rights change, then different connections in the pool may behave differently, which will most likely lead to a debug nightmare, to say the least. Of course nothing prevents you from changing other pieces in the DSN, like the database/schema that you connect to, but that's more often than not a very bad idea. (To start with, all of your queries would have to be fully qualified. So, don't do it unless you know exactly what you're doing.)

Like any driver, you don't use this package directly. The only difference with respecto to other drivers is that this is not automatically available simply by importing the package. The reason is that this driver needs something else to work with: a DSN provider. That's the place where you define the actual DSN that you want to use, starting from the DSN that the database/sql package provides (that matches what you give in sql.Open). You can use any type that implements the DSNProvider interface. For extra convenience, a DSNProviderFunc allows you to define a single function inline, instead of having to declare a type and methods. Using it, your application may look like this:

import (
	"database/sql"
	"time"

	"github.com/gkristic/lazydsn"
	"github.com/go-sql-driver/mysql"
)

const alias = "lazydsn:mysql"

func main() {
	lazydsn.Register(alias, &mysql.MySQLDriver{},
		lazydsn.DSNProviderFunc(func (dsn string) (string, error) {
			// Compute a new dsn; e.g., by using AWS Secrets Manager
			return dsn, nil
		}),
	)

	db := sql.Open(alias, "arn:...")
	db.SetConnMaxLifetime(time.Hour)
	// Keep working with db as usual
}

Once you open the database with this driver and set the connection lifetime, everything looks just as usual from the application's perspective. However, behind the scenes, connections have a predefined expiration, and they are renewed using the latest credentials available. Credentials rotation is thus fully suported, but completely transparent.

If the type that you provide also implements FullDSNProvider, then a cancellation context will be provided when available. Again, for convenience, you can use a DSNProviderWCFunc to give your context-enabled function inline.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Register

func Register(alias string, d driver.Driver, dsnp DSNProvider)

Register creates and registers the driver under the provided alias, with the given inner driver and DSN provider. Applications don't typically register drivers directly, relying on implicit registration via package import and drivers' init functions. However, this driver requires parameters (the DSN provider), that applications have to provide for this driver to be meaningful at all. It's a good practice to register this as close to the most basic packages in your application as possible, to separate business code from the intricacies of dealing with database drivers.

Types

type DSNProvider

type DSNProvider interface {
	FetchDSN(string) (string, error)
}

A DSNProvider allows converting a DSN, as provided to this driver via database/sql, into a DSN suitable for using with the inner driver.

type DSNProviderFunc

type DSNProviderFunc func(string) (string, error)

DSNProviderFunc provides a convenient type so that applications don't have to declare specific types and methods with the only purpose of having a DSNProvider. This makes it possible to use an inline function literal instead.

func (DSNProviderFunc) FetchDSN

func (f DSNProviderFunc) FetchDSN(dsn string) (string, error)

FetchDSN exercises the original function to resolve a DSN.

type DSNProviderWCFunc

type DSNProviderWCFunc func(context.Context, string) (string, error)

DSNProviderWCFunc provides a convenient type so that applications don't have to declare specific types and methods with the only purpose of having a DSNProvider. This makes it possible to use an inline function literal instead.

func (DSNProviderWCFunc) FetchDSN

func (f DSNProviderWCFunc) FetchDSN(dsn string) (string, error)

FetchDSN exercises the function providing an empty context.

func (DSNProviderWCFunc) FetchDSNWithContext

func (f DSNProviderWCFunc) FetchDSNWithContext(ctx context.Context, dsn string) (string, error)

FetchDSNWithContext exercises the original function, providing the context passed in to this function by the driver.

type Driver

type Driver struct {
	driver.Driver
	// contains filtered or unexported fields
}

Driver is not a database driver by itself, but rather a wrapper on top of any driver that can be used with Go's database/sql. It adds the capability to resolve DSNs at the moment that each individual connection in the pool is opened. Contrast with the typical database/sql behavior, where every new connection in the pool is created under the same DSN, set at the time the database was sql.Open'ed.

func New

func New(d driver.Driver, dsnp DSNProvider) *Driver

New creates a new driver with the given inner driver d and DSN provider. Note that d can be a driver for any database; a Driver is database agnostic. This does NOT register the driver with database/sql. See Register. This function is provided so that other packages are able to create a properly initialized driver, in case they want to extend it (just like we're doing here with other drivers!)

func (*Driver) Open

func (d *Driver) Open(dsn string) (driver.Conn, error)

Open opens a database connection and returns the latter as a driver.Conn type. This is part of the driver.Driver interface. The DSN provided to this driver doesn't need to follow the format imposed by the underlying driver. In fact, it can be completely different; no restrictions are enforced by this package. The translation from the DSN provided by database/sql to this function and the one needed by the inner driver is entirely done by the DSNProvider assigned to this driver.

func (*Driver) OpenConnector

func (d *Driver) OpenConnector(dsn string) (driver.Connector, error)

OpenConnector returns a driver.Connector that can be used to open connections to the database without having the inner driver parsing the DSN repeatedly. That's, of course, as long as the inner driver implements the driver.DriverContext interface. If not, the resulting connector will simply be wrapping the Open method.

type FullDSNProvider

type FullDSNProvider interface {
	DSNProvider
	FetchDSNWithContext(context.Context, string) (string, error)
}

A FullDSNProvider acts like a DSNProvider, but provides an additional function to fetch the DSN using a context, thus allowing the chance to cancel the operation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL