dbfwd

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: MIT Imports: 10 Imported by: 0

README


Unit Tests Go Reference Releases Discord Discussion

About

dbfwd connects a program such as usql to a database that runs on Kubernetes. It reads a URL such as k+cnpg://pg.prod/app or k://pg-rw.prod/, finds the database, starts a port-forward, and gives back an ordinary URL for a local port. The URL that the caller writes holds no password, because dbfwd reads the password from the cluster.

dbfwd picks the pod it forwards to and watches it. When the pod goes, such as after a restart or a failover, dbfwd forwards to a new pod on the same local port, and the URL keeps working.

It supports every tier 1 operator in docs/OPERATORS.md, and a database with no operator, through its Service, StatefulSet, Deployment, Helm release or pod. It is an early implementation for Kubernetes. docs/PLAN.md says where it stands.

Installing

The package:

go get github.com/xo/dbfwd

The command, which runs the native client of a database or usql through a forward:

go install github.com/xo/dbfwd/cmd/dbfwd@latest

dbfwd runs kubectl, so kubectl must be on the path, with a kubeconfig that reaches the cluster.

Using

Run the client of a database through a forward:

dbfwd k+cnpg://pg.prod/

From Go, Resolve returns a Forward, which holds the local URL and stops the forward when it closes:

import (
    "github.com/xo/dbfwd"
    _ "github.com/xo/dbfwd/k8s"
    "github.com/xo/dburl"
)

u, err := dburl.Parse("k+cnpg://pg.prod/app")
if err != nil {
    return err
}
fwd, err := dbfwd.Resolve(ctx, u)
if err != nil {
    return err
}
defer fwd.Close()
local, err := fwd.URL(ctx)
if err != nil {
    return err
}
db, err := sql.Open(local.Driver, local.DSN)

docs/API.md holds the API, and docs/CLI.md holds the command.

Documents

Document What it holds
docs/PLAN.md The aim, where the work stands, what other projects give, and the next steps
docs/API.md The Go API of the package
docs/URL.md The URL that dbfwd reads
docs/LOOKUP.md How dbfwd finds a database from a name, with or without an operator
docs/OPERATORS.md What each supported operator creates, and where dbfwd reads it
docs/CLI.md The dbfwd command, which runs a native client or usql through a forward
docs/LIST.md Listing the databases of a cluster, for dbfwd list and the \k command of usql
docs/TESTING.md How dbfwd is tested, locally and in CI
docs/decisions/ Every decision, one file each
AGENTS.md The rules for a coding agent. Every rule applies to a person too
CONTRIBUTING.md How to send a change

dbfwd is one of a set of packages that each do one part of the job, so that a client can take the parts it needs.

  • usql is a command line client for many databases. It calls dbfwd for a k8s URL and holds the Forward for the session.
  • dburl parses a database URL, including the k8s scheme and its aliases. dbfwd reads the URL that dburl parsed, and never parses one.
  • dbmeta reads database metadata. Its dbrun starts the kind cluster that the tests of dbfwd run in, and its client table names the native client that the dbfwd command runs.

Contributing

Read AGENTS.md first. It holds the rules, including the ones that are not obvious: the standard library and dburl only in what usql imports, no cgo, no build constraint on an operating system or an architecture, and no password in a log, an error or the arguments of a child process.

Run the checks before you send a change, with the same flags CI uses:

gofmt -l . && go vet ./... && go build ./... && go test -race -count=2 ./...

Tests in the root module never need a cluster. They run against a fake kubectl, or replay what a real cluster returned, from testdata/. The cluster tests are in the test module, and docs/TESTING.md says how to run them.

License

MIT. See LICENSE.


Documentation

Overview

Package dbfwd connects a program to a database that is reachable only through a forward, such as a database that runs on Kubernetes.

Resolve reads a URL that github.com/xo/dburl parsed, such as k+cnpg://pg.prod/app, finds the database and its credentials, starts the forward, and returns a Forward with an ordinary URL for a local port. The URL holds no password. The backend finds it.

A backend registers itself from init. A program imports the backends it uses:

import _ "github.com/xo/dbfwd/k8s"

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Handles

func Handles(u *dburl.URL) bool

Handles reports whether a backend is registered for the scheme of u. A caller, such as usql, uses it in place of a list of schemes of its own.

func ListURL added in v0.3.0

func ListURL(backend, scope string, where ...Field) (*dburl.URL, error)

ListURL builds the URL of a listing for the backend, such as "k8s", from a scope that a person typed and the fields that edit it, so that every caller reads a scope in the same way. The backend owns the rules, and docs/LIST.md holds them. A field that disagrees with the scope is an error that wraps ErrBadURL.

func Register

func Register(name string, b Backend)

Register makes a backend available for the scheme name, such as "k8s". It panics when the name is empty or taken, or the backend is nil. Call it from init.

Types

type Backend

type Backend interface {
	// Lookup runs the lookup for u.
	Lookup(ctx context.Context, u *dburl.URL, cfg Config) (*Match, error)

	// Open starts a forward to the database that u names, and returns once it
	// is ready.
	Open(ctx context.Context, u *dburl.URL, cfg Config) (*Opened, error)
}

Backend finds a database and starts a forward to it. A backend registers itself from init with Register.

type Config

type Config struct {
	// Tools maps the name of a tool, such as kubectl, to its path.
	Tools map[string]string

	// Env holds the variables that WithEnv set, as KEY=value, in order.
	Env []string

	// Interactive runs a child process with the terminal, or is nil.
	Interactive func(run func(tty *os.File) error) error
}

Config is the configuration of one call, built from its options. A caller never builds one. A backend reads it.

func (Config) Tool

func (cfg Config) Tool(name string) string

Tool returns the path of the tool name, or name itself when no option set a path, so that exec looks for it on the PATH.

type Entry added in v0.2.0

type Entry struct {
	// Target is the object that the row stands for, as Lookup reports it.
	Target Target

	// Operator is the name of the operator that manages the database, such
	// as "CloudNativePG", or empty when none does.
	Operator string

	// URL is the shortest URL that Resolve maps to the same database, such
	// as "k://pg.prod/". It holds no password.
	URL string

	// Roles holds the roles that the database serves, such as "rw" and "ro".
	Roles []string

	// Ready is the number of pods that are ready, and Pods the number of
	// pods.
	Ready, Pods int

	// Databases holds the databases that the rules name, or none.
	Databases []string

	// Credential says where Resolve reads the password, such as
	// "Secret pg-app key password". It never holds a secret value.
	Credential string
}

Entry is one database that List found.

type Error

type Error string

Error is an error.

const (
	// ErrNoBackend is the error for a URL whose scheme has no backend.
	ErrNoBackend Error = "no backend for the scheme"
	// ErrNoTool is the error when a tool that the backend runs, such as
	// kubectl, is missing or cannot run. The error that wraps it names the
	// tool.
	ErrNoTool Error = "tool is missing or cannot run"
	// ErrUnauthorized is the error when the credentials of the person were
	// refused, because they expired or a login failed. The fix is to log in
	// again.
	ErrUnauthorized Error = "not logged in"
	// ErrForbidden is the error when a step was not allowed, by RBAC or by
	// IAM. The fix is to ask for access.
	ErrForbidden Error = "not allowed"
	// ErrNotFound is the error when no step of the lookup matched.
	ErrNotFound Error = "not found"
	// ErrAmbiguous is the error when a step matched more than one object, or
	// the signals for the type disagree.
	ErrAmbiguous Error = "more than one match"
	// ErrUnsupported is the error for a database that no database/sql driver
	// can reach, such as MongoDB, or for a target that a forward cannot reach,
	// such as an ExternalName Service.
	ErrUnsupported Error = "not supported"
	// ErrRoleConflict is the error when role disagrees with the target, such
	// as role=ro with the Service pg-rw.
	ErrRoleConflict Error = "role conflicts with the target"
	// ErrQueryKey is the error when a driver key has the name of a key of the
	// backend.
	ErrQueryKey Error = "query key clashes with a key of the backend"
	// ErrBadURL is the error when a URL cannot be read. It names no part of
	// the URL, because a local URL holds a password.
	ErrBadURL Error = "the URL is not valid"
	// ErrForwardLost is the error when a running forward stops on its own.
	ErrForwardLost Error = "forward lost"
	// ErrClosed is the error when a forward is used after Close.
	ErrClosed Error = "forward closed"
)

Error values.

func (Error) Error

func (err Error) Error() string

Error satisfies the error interface.

type Field

type Field struct {
	Name  string
	Value string
}

Field is one named value of a Scope.

type Forward

type Forward struct {
	// User is the user the URL logs in as.
	User string

	// Database is the database the URL opens. It is empty when the database
	// has none.
	Database string

	// Target is the object that the lookup matched.
	Target Target
	// contains filtered or unexported fields
}

Forward is a running forward to a database. Its methods are safe to call from more than one goroutine.

func Resolve

func Resolve(ctx context.Context, u *dburl.URL, opts ...Option) (*Forward, error)

Resolve finds the database that u names, starts a forward to it, and returns the Forward. It returns when the forward is ready.

The backend is chosen by u.SchemeName. Canceling ctx before Resolve returns stops it and releases everything it started. After Resolve returns, ctx no longer controls the forward, and Forward.Close does.

func (*Forward) Close

func (f *Forward) Close() error

Close stops the forward and releases the local port. It is safe to call more than once.

func (*Forward) Done

func (f *Forward) Done() <-chan struct{}

Done returns a channel that is closed when the forward stops, by Close or because it was lost.

func (*Forward) Err

func (f *Forward) Err() error

Err returns nil while the forward runs and after Close. It returns why the forward was lost, as an error that wraps ErrForwardLost.

func (*Forward) GoString

func (f *Forward) GoString() string

GoString returns the same as String, so that %#v holds no password.

func (*Forward) LogValue

func (f *Forward) LogValue() slog.Value

LogValue returns the target for log/slog. It never holds the password.

func (*Forward) String

func (f *Forward) String() string

String returns the target, such as "Service pg-rw in context kind-prod, namespace prod, owned by CloudNativePG Cluster pg". It never holds the password.

func (*Forward) URL

func (f *Forward) URL(ctx context.Context) (*dburl.URL, error)

URL returns the URL of the local end, such as postgres://app:...@127.0.0.1:41234/app. It holds the password, so it must never be printed or logged. When the credential of the backend expires, URL makes a new one first. After the forward is lost, it returns an error that wraps ErrForwardLost.

Call URL for each new connection, and do not keep its result. It is cheap when nothing expired, and safe to call from many goroutines at once.

type Lister added in v0.2.0

type Lister interface {
	// List lists the databases that u names.
	List(ctx context.Context, u *dburl.URL, cfg Config) (*Listing, error)

	// ListURL builds the URL of a listing from a scope and the fields that
	// edit it. It returns the URL as a string for dburl.Parse.
	ListURL(scope string, where []Field) (string, error)
}

Lister is a Backend that can list the databases it reaches (D18).

type Listing added in v0.2.0

type Listing struct {
	// Scope is where List looked, as Target.Scope holds it, with the context
	// and the namespace resolved. With every namespace, the namespace is "*".
	Scope []Field

	// Entries holds one entry for each database, sorted by scope, then by
	// name.
	Entries []Entry

	// Skipped holds each part of the scope that List did not read, such as a
	// kind that RBAC does not allow.
	Skipped []Skip
}

Listing is what List found.

func List added in v0.2.0

func List(ctx context.Context, u *dburl.URL, opts ...Option) (*Listing, error)

List lists the databases that u names. The name and the namespace in u can be "*", and the name can hold "*" as a wildcard, such as k://*, k://*.prod or k://pg*.prod. List reads no credential and starts no forward.

List fails only when it read nothing, or when RBAC forbids the scope that u asks for. When it read some of the scope, it lists the rest in Skipped.

type LookupError

type LookupError struct {
	// Name is the name in the URL.
	Name string

	// Scope is where the lookup looked.
	Scope []Field

	// Steps is each step that was tried.
	Steps []Step

	// Err is the error value, such as ErrNotFound.
	Err error
}

LookupError is the error of a lookup that failed. It names what was tried, and wraps one of the error values.

func (*LookupError) Error

func (err *LookupError) Error() string

Error satisfies the error interface.

func (*LookupError) Unwrap

func (err *LookupError) Unwrap() error

Unwrap returns the error value.

type Match

type Match struct {
	// Target is the object that matched.
	Target Target

	// Steps is each step that Lookup tried, in order.
	Steps []Step
}

Match is what Lookup found.

func Lookup

func Lookup(ctx context.Context, u *dburl.URL, opts ...Option) (*Match, error)

Lookup runs the lookup for u and reports what matched, with each step it tried. It reads no credential and starts no forward.

type Opened

type Opened struct {
	// URL is the URL of the local end, as a string for dburl.Parse. It holds
	// the password.
	URL string

	// Expires is when the credential in URL expires. It is zero when the
	// credential does not expire.
	Expires time.Time

	// Renew makes a new URL when the credential expires, and returns when the
	// new one expires. It is nil when the credential does not expire.
	Renew func(ctx context.Context) (string, time.Time, error)

	// User is the user the URL logs in as.
	User string

	// Database is the database the URL opens, or empty.
	Database string

	// Target is the object that the lookup matched.
	Target Target

	// Tunnel is the running forward.
	Tunnel Tunnel
}

Opened is what a backend returns from Open.

type Option

type Option func(*Config)

Option sets one value of the Config of a call of Resolve or Lookup.

func WithEnv

func WithEnv(key, value string) Option

WithEnv sets the environment variable key to value for every child process of the call, such as WithEnv("KUBECONFIG", path). With no such option, a child process gets the environment of the caller.

func WithInteractive

func WithInteractive(fn func(run func(tty *os.File) error) error) Option

WithInteractive runs each child process that can prompt inside fn, so that a credential plugin or a login can use the terminal. With no such option, no child process gets a terminal.

func WithTool

func WithTool(name, path string) Option

WithTool runs the tool name from path, such as WithTool("kubectl", "/opt/bin/kubectl"). With no such option, a backend looks for the tool on the PATH.

type Skip added in v0.2.0

type Skip struct {
	// Scope is where, such as the namespace "prod".
	Scope []Field

	// Kind is what List tried to read there, such as "services".
	Kind string

	// Reason says why, such as "not allowed".
	Reason string
}

Skip is a part of the scope that List did not read.

type Step

type Step struct {
	// Kind is the kind that the step looked for, such as "Helm release".
	Kind string

	// Result says what the step found, such as "no match", "not installed",
	// "not allowed" or "match".
	Result string
}

Step is one step of a lookup.

type Target

type Target struct {
	// Backend is the name of the backend, such as "k8s".
	Backend string

	// Scope is where the object lives, from the outside in, such as the
	// context and the namespace for Kubernetes.
	Scope []Field

	// Kind is the kind of the object that matched, such as "Service".
	Kind string

	// Name is the name of the object that matched, such as "pg-rw".
	Name string

	// Owner is what manages the object, such as "CloudNativePG Cluster pg".
	// It is empty when nothing manages it.
	Owner string

	// Type is the database type, such as "postgres".
	Type string
}

Target is the object that a lookup matched.

func (Target) String

func (t Target) String() string

String returns the target, such as "Service pg-rw in context kind-prod, namespace prod, owned by CloudNativePG Cluster pg".

type Tunnel

type Tunnel interface {
	// Done returns a channel that is closed when the tunnel stops.
	Done() <-chan struct{}

	// Err returns why the tunnel stopped, after Done is closed.
	Err() error

	// Close stops the tunnel. It is safe to call more than once.
	Close() error
}

Tunnel is the running forward of a backend.

Directories

Path Synopsis
cmd
dbfwd command
Command dbfwd connects the native client of a database, or usql, to a database that is reachable only through a forward, such as a database that runs on Kubernetes.
Command dbfwd connects the native client of a database, or usql, to a database that is reachable only through a forward, such as a database that runs on Kubernetes.
Package k8s is the Kubernetes backend of dbfwd.
Package k8s is the Kubernetes backend of dbfwd.

Jump to

Keyboard shortcuts

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