tailsql

package
v0.0.0-...-77aec5a Latest Latest
Warning

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

Go to latest
Published: May 22, 2026 License: BSD-3-Clause Imports: 30 Imported by: 2

Documentation

Overview

Package tailsql implements an HTTP API and "playground" UI for sending SQL queries to a collection of local and/or remote databases, and rendering the results for human consumption.

API

The main UI is served from "/", static assets from "/static/". The following path and query parameters are understood:

  • The q parameter carries an SQL query. The syntax of the query depends on the src (see below). See also "Named Queries" below.

  • The src parameter names which database to query against. Its values are defined when the server is set up. If src is omitted, the first database is used as a default.

  • "/" serves output as HTML for the UI. In this format the query (q) may be empty (no output will be displayed).

  • "/json" serves output as JSON objects, one per row. In this format the query (q) must be non-empty.

  • "/csv" serves output as comma-separated values, the first line giving the column names, the remaining lines the rows. In this format the query (q) must be non-empty.

  • "/meta" serves a JSON blob of metadata about available data sources.

Calls to the /json endpoint must set the Sec-Tailsql header to "1". This prevents browser scripts from directing queries to this endpoint.

Calls to /csv must either set Sec-Tailsql to "1" or include a tailsqlQuery=1 same-site cookie.

Calls to the UI with a non-empty query must include the tailsqlQuery=1 same-site cookie, which is set when the UI first loads. This averts simple cross-site redirection tricks.

Named Queries

The query processor treats a query of the form "named:<string>" as a named query. Named queries are SQL queries pre-defined by the database, to allow users to make semantically stable queries without relying on a specific schema format.

Meta Queries

The query processor treats a query "meta:named" as a meta-query to report the names and content of all named queries, regardless of source.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func UnmarshalOptions

func UnmarshalOptions(data []byte, opts *Options) error

UnmarshalOptions unmarshals a HuJSON Config value into opts.

Types

type DBOptions

type DBOptions struct {
	// Label is a human-readable descriptive label to show to users when
	// rendering this database in a UI.
	Label string

	// NamedQueries is a map from names to SQL query text, that the service
	// should allow as pre-defined queries for this database.
	//
	// Unlike user-saved queries, named queries allow the database to change the
	// query when the underlying schema changes while preserving the semantics
	// the user observes.
	NamedQueries map[string]string
}

DBOptions are optional settings for a database. A nil *DBoptions is ready for use and provides defaults as described.

type DBSpec

type DBSpec struct {
	Source string `json:"source"`           // UI slug (required)
	Label  string `json:"label,omitempty"`  // descriptive label
	Driver string `json:"driver,omitempty"` // e.g., "sqlite", "snowflake"

	// Named is an optional map of named SQL queries the database should expose.
	Named map[string]string `json:"named,omitempty"`

	URL     string    `json:"url,omitempty"`     // path or connection URL
	KeyFile string    `json:"keyFile,omitempty"` // path to key file
	Secret  string    `json:"secret,omitempty"`  // name of secret
	DB      Queryable `json:"-"`                 // programmatic data source
}

A DBSpec describes a database that the server should use.

The Source must be non-empty, and exactly one of URL, KeyFile, Secret, or DB must be set.

  • If DB is set, it is used directly as the database to query, and no connection is established.

Otherwise, the Driver must be non-empty and the database/sql library is used to open a connection to the specified database:

  • If URL is set, it is used directly as the connection string.

  • If KeyFile is set, it names the location of a file containing the connection string. If set, KeyFile is expanded by os.ExpandEnv.

  • Otherwise, Secret is the name of a secret to fetch from the secrets service, whose value is the connection string. This requires that a secrets server be configured in the options.

type Duration

type Duration time.Duration

Duration is a wrapper for a time.Duration that allows it to marshal more legibly in JSON, using the standard Go notation.

func (Duration) Duration

func (d Duration) Duration() time.Duration

Duration converts d to a standard time.Duration.

func (Duration) MarshalText

func (d Duration) MarshalText() ([]byte, error)

func (*Duration) UnmarshalText

func (d *Duration) UnmarshalText(data []byte) error

type LocalClient

type LocalClient interface {
	WhoIs(context.Context, string) (*apitype.WhoIsResponse, error)
}

LocalClient is the subset of the tailscale.LocalClient interface required by the Server. It is defined here so that the caller can substitute a fake for testing and debugging.

type Options

type Options struct {
	// The tailnet hostname the server should run on (tsnet).
	Hostname string `json:"hostname,omitempty"`

	// The directory for tailscale state and configurations (tsnet).
	StateDir string `json:"stateDir,omitempty"`

	// If true, serve HTTPS instead of HTTP (tsnet).
	ServeHTTPS bool `json:"serveHTTPS,omitempty"`

	// The URL of the coordination server to use (tsnet).
	// If empty, the Tailscale default is used.
	ControlURL string `json:"controlURL,omitempty"`

	// If non-empty, a SQLite database URL to use for local state.
	LocalState string `json:"localState,omitempty"`

	// If non-empty, and LocalState is defined, export a read-only copy of the
	// local state database as a source with this name.
	LocalSource string `json:"localSource,omitempty"`

	// Databases that the server will allow queries against (optional).
	Sources []DBSpec `json:"sources,omitempty"`

	// Additional links that should be propagated to the UI.
	UILinks []UILink `json:"links,omitempty"`

	// If set, prepend this prefix to each HTTP route. By default, routes are
	// anchored at "/".
	RoutePrefix string `json:"routePrefix,omitempty"`

	// The maximum timeout for a database query (0 means no timeout).
	QueryTimeout Duration `json:"queryTimeout,omitempty"`

	// A connection to tailscaled for authorization checks. If nil, no
	// authorization checks are performed and all requests are permitted.
	LocalClient LocalClient `json:"-"`

	// If non-nil, the server will add metrics to this map. The caller is
	// responsible for ensuring the map is published.
	Metrics *expvar.Map `json:"-"`

	// If non-nil and a LocalClient is available, Authorize is called for each
	// request giving the requested database src and the caller's WhoIs record.
	// If it reports an error, the request is failed.
	//
	// If Authorize is nil and a LocalClient is available, the default rule is
	// to accept any logged-in user, rejecting tagged nodes.
	//
	// If no LocalClient is available, this field is ignored, no authorization
	// checks are performed, and all requests are accepted.
	Authorize func(src string, info *apitype.WhoIsResponse) error `json:"-"`

	// If non-nil, use this store to fetch secret values. This is required if
	// any of the sources specifies a named secret for its connection string.
	SecretStore *setec.Store `json:"-"`

	// Optional rules to apply when rendering text for presentation in the UI.
	// After generating the value string, each rule is matched in order, and the
	// first match (if any) is applied to rewrite the output. The value returned
	// by the rule replaces the original string.
	UIRewriteRules []UIRewriteRule `json:"-"`

	// If non-nil, call this function with each query presented to the API.  If
	// the function reports an error, the query fails; otherwise the returned
	// query state is used to service the query.  If nil, DefaultCheckQuery is
	// used.
	CheckQuery func(Query) (Query, error) `json:"-"`

	// If non-nil, send logs to this logger. If nil, use log.Printf.
	Logf logger.Logf `json:"-"`
}

Options describes settings for a Server.

The fields marked as "tsnet" are not used directly by tailsql, but are provided for the convenience of a main program that wants to run the server under tsnet.

func (Options) CheckSources

func (o Options) CheckSources() ([]string, error)

CheckSources validates the sources of o. If this succeeds, it also returns a slice of any secret names required by the specified sources, if any.

type Query

type Query struct {
	Source string // the data source requested
	Query  string // the text of the query
}

A Query carries the parameters of a query presented to the API.

func DefaultCheckQuery

func DefaultCheckQuery(q Query) (Query, error)

DefaultCheckQuery is the default query check function used if another is not specified in the Options. It accepts all queries for all sources, as long as the query text does not exceed 4000 bytes.

type Queryable

type Queryable interface {
	// Query issues the specified SQL query in a transaction and returns the
	// matching result set, if any.
	Query(ctx context.Context, sql string, params ...any) (RowSet, error)

	// Close closes the database.
	Close() error
}

Queryable is the interface used to issue SQL queries to a database.

type RowSet

type RowSet interface {
	// Columns reports the names of the columns requested by the query.
	Columns() ([]string, error)

	// Close closes the row set, preventing further enumeration.
	Close() error

	// Err returns the error, if any, that was encountered during iteration.
	Err() error

	// Next prepares the next result row for reading by the Scan method, and
	// reports true if this was successful or false if there was an error or no
	// more rows are available.
	Next() bool

	// Scan copies the columns of the currently-selected row into the values
	// pointed to by its arguments.
	Scan(...any) error
}

A RowSet is a sequence of rows reported by a query. It is a subset of the interface exposed by database/sql.Rows, and the implementation must provide the same semantics for each of these methods.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a server for the tailsql API.

func NewServer

func NewServer(opts Options) (*Server, error)

NewServer constructs a new server with the given Options.

func (*Server) Close

func (s *Server) Close() error

Close closes all the database handles held by s and returns the join of their errors.

func (*Server) NewMux

func (s *Server) NewMux() *http.ServeMux

NewMux constructs an HTTP router for the service.

func (*Server) SetDB

func (s *Server) SetDB(source string, db *sql.DB, opts *DBOptions) bool

SetDB adds or replaces the database associated with the specified source in s with the given open db and options. See Server.SetSource.

func (*Server) SetSource

func (s *Server) SetSource(source string, db Queryable, opts *DBOptions) bool

SetSource adds or replaces the database associated with the specified source in s with the given open db and options.

If a database was already open for the given source, its value is replaced, the old database handle is closed, and SetDB reports true.

If no database was already open for the given source, a new source is added and SetSource reports false.

type UILink struct {
	Anchor string `json:"anchor"`
	URL    string `json:"url"`
}

UILink carries anchor text and a target URL for a hyperlink.

type UIRewriteRule

type UIRewriteRule struct {
	Column *regexp.Regexp // pattern for the column name (nil matches all)
	Value  *regexp.Regexp // pattern for the value (nil matches all)

	// The Apply function takes the name of a column, the input value, and the
	// result of matching the value regexp (if any). Its return value replaces
	// the input when the value is rendered. If Apply == nil, the input is not
	// modified.
	//
	// As a special case, if Apply returns a nil value, the rule evaluator skips
	// the rule as if it had not matched, and goes on to the next rule.
	Apply func(column, input string, valueMatch []string) any
}

UIRewriteRule is a rewriting rule for rendering output in HTML.

A rule matches a value if:

  • Its Column regexp is empty or matches the column name, and
  • Its Value regexp is empty or matches the value string

If a rule matches, its Apply function is called.

func (UIRewriteRule) CheckApply

func (u UIRewriteRule) CheckApply(column, input string) (bool, any)

CheckApply reports whether u matches the specified column and input, and if so returns the result of applying u to it.

Jump to

Keyboard shortcuts

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