kubedial

package
v1.0.13 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package kubedial reaches a CloudNativePG database in a Kubernetes namespace through the kube API server, with no network path of its own: it resolves a ready pod behind the database's Service, opens a pods/portforward stream to it and hands the stream to pgx as a net.Conn.

It is the transport under [dbquery.Run] wherever the database is not reachable by DNS and IP: `forge db query` against a developer's k3d, and the control plane's QueryDatabase against a customer environment's namespace on another cluster. It lives beside pkg/dbquery rather than in it so that dbquery's core stays free of client-go.

What a connection is

rest.Config ──► EndpointSlices of <db>-ro (or <db>-rw)
                  └─ a ready pod, chosen per connection
            ──► pods/portforward to that pod, port 5432
                  └─ one SPDY-over-websocket (plain SPDY as fallback)
                     connection and one stream pair per pgx connection
            ──► a net.Conn (pgx's DialFunc)

There is no local listener: the stream is the connection. A pool therefore holds one API-server connection per database connection, and the pod is re-resolved on every dial, so a failover is followed by the next connection. A port-forward enters the pod's network namespace through the kubelet, so a NetworkPolicy that admits only same-namespace pods does not stand in the way, which is also why the Kubernetes Role below is the access boundary.

Choosing the Service

ServiceName mirrors the rule `forge db query` has always used: <name>-ro when the Cluster runs standbys (instances > 1), otherwise <name>-rw, so a read never loads the primary when a replica exists.

TLS

CNPG serves TLS and its pg_hba admits remote clients over hostssl only. [Credentials.ConnConfig] verifies the server certificate against the cluster CA (CASecretName) with ServerName <service>.<namespace>.svc, a name CNPG's server certificate carries, so sslmode=verify-full holds inside the tunnel and the API server's own TLS is not what protects the session.

Kubernetes Role rules the caller needs

PolicyRules returns the exact rules, in a namespaced Role, for the databases the caller may reach. Declare them from that one source:

pods/portforward   get, create   a port-forward is a GET upgrade to
                                 websocket and a POST for SPDY; since
                                 Kubernetes 1.35 the websocket upgrade also
                                 needs create (the API server's pod-websocket-upgrade create-permission gate)
endpointslices     get, list     (discovery.k8s.io) to find ready pods
secrets            get           on exactly <db>-forge-ro and <db>-ca

pods/portforward cannot be narrowed to a port or a pod by a Role, so it admits any port of any pod in the namespace. Give the identity that dials its own Role and ServiceAccount (or impersonation) rather than widening a deploy identity, and rely on the API server's audit log as the second trail.

The <db>-ca Secret holds the CA's private key beside its certificate. This package reads only ca.crt and keeps nothing else, but the Secret's get permission is a real grant: scope it to the names, never to all Secrets.

Index

Constants

View Source
const (
	SSLModeDisable    = "disable"
	SSLModeRequire    = "require"
	SSLModeVerifyFull = "verify-full"
)

SSL modes ConnConfigForAddr speaks. The empty string means SSLModeVerifyFull.

View Source
const PostgresPort = 5432

PostgresPort is the port a CNPG instance serves on.

Variables

View Source
var ErrNoDashboardCredentials = errors.New("no dashboard credentials")

ErrNoDashboardCredentials means the dashboard role's Secret is missing or holds no password: the database never opted into dashboards (ManagedDatabase.dashboards), or the deploy that enables them has not run. A distinct error from ErrNoReadOnlyCredentials because the remedies differ: this one is usually the declaration, not a stale deploy.

View Source
var ErrNoReadOnlyCredentials = errors.New("no read-only credentials")

ErrNoReadOnlyCredentials means the read-only role's Secret is missing or holds no password: the database predates the forge_ro role, or CNPG has not reconciled it yet.

View Source
var ErrNoReadyEndpoint = errors.New("no ready endpoint")

ErrNoReadyEndpoint means the Service has no ready pod to forward to: the Cluster is starting, scaled to zero, or the Service does not exist.

Functions

func CASecretName

func CASecretName(name string) string

CASecretName is the Secret CNPG keeps the cluster CA in (key ca.crt) for the Cluster called name.

func ConnConfigForAddr

func ConnConfigForAddr(db Database, creds *Credentials, addr, sslmode string) (*pgx.ConnConfig, error)

ConnConfigForAddr is Client.ConnConfigFor for a caller that brings its own network path to the database (a VPN, a kubectl port-forward, a bastion): it dials addr (host:port) directly, with no tunnel of its own. sslmode is SSLModeVerifyFull by default, verified against the cluster CA with ServerNameForAddr as the server name; SSLModeRequire encrypts without verifying, SSLModeDisable does not encrypt.

It makes no API call: the credentials are the caller's to read (Client.Credentials). The config holds the password; do not log it.

func PolicyRules

func PolicyRules(databases ...string) []rbacv1.PolicyRule

PolicyRules are the namespaced Kubernetes Role rules an identity needs to use this package against the named databases (CNPG Cluster names) in one namespace. Put them in a Role (or a ClusterRole bound by RoleBinding) in each database namespace; declare them from here so the grant and the code that needs it cannot drift.

pods/portforward  get, create  the websocket upgrade is a GET and, since
                               Kubernetes 1.35, also checks create; SPDY is a POST
endpointslices    get, list    discovery.k8s.io: find the ready pods
secrets           get          only <db>-forge-ro and <db>-ca, by name
                               (<db>-forge-dash for [PolicyRulesFor] [RoleDashboard])

pods/portforward is not restrictable by port or pod: whoever holds it can reach any port of any pod in the namespace. Bind these rules to an identity that only runs database queries.

With no names the secrets rule is omitted (a rule with an empty resourceNames would grant every Secret): an identity that gets no secret access cannot call Client.Credentials.

func PolicyRulesFor added in v1.0.9

func PolicyRulesFor(role Role, databases ...string) []rbacv1.PolicyRule

PolicyRulesFor is PolicyRules for an identity that connects as role: the secrets rule names that role's Secret (<db>-forge-ro or <db>-forge-dash) and the CA, never the other role's. An identity that runs dashboard panels therefore cannot read forge_ro's password, and one that runs `forge db query` cannot read forge_dash's. Give an identity that needs both the union of the two calls' rules.

func ServerNameForAddr

func ServerNameForAddr(db Database) string

ServerNameForAddr is the name the server certificate is verified against when the caller reaches db at an address of its own: the CNPG read-write Service, <name>-rw.<namespace>.svc, which every CNPG server certificate carries. A port-forward to localhost therefore still verifies.

func ServiceName

func ServiceName(name string, instances int32) string

ServiceName is the CNPG Service a read goes to for the Cluster called name: name-ro when it runs standbys, name-rw otherwise.

Types

type Client

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

Client dials databases through one cluster's API server. It is safe for concurrent use.

func New

func New(cfg *rest.Config) (*Client, error)

New builds a Client over the cluster cfg points at.

func NewWithClientset

func NewWithClientset(cs kubernetes.Interface, pods PodDialer) *Client

NewWithClientset builds a Client over an existing clientset (reads) and an explicit PodDialer (streams). With a fake clientset and a fake PodDialer no cluster is needed.

func (*Client) ConnConfig

func (c *Client) ConnConfig(ctx context.Context, db Database) (*pgx.ConnConfig, error)

ConnConfig is everything needed to run a query against db: it reads the credentials and returns a pgx config that dials through the tunnel and verifies the server certificate against the cluster CA (sslmode=verify-full semantics, ServerName <service>.<namespace>.svc).

Pass it to pgx.ConnectConfig, or to a pgxpool.Config's ConnConfig: every connection takes its own stream and re-resolves the pod. The config holds the password; do not log it.

func (*Client) ConnConfigFor

func (c *Client) ConnConfigFor(db Database, creds *Credentials) (*pgx.ConnConfig, error)

ConnConfigFor is Client.ConnConfig with credentials the caller already holds, for a pool that caches them.

func (*Client) Credentials

func (c *Client) Credentials(ctx context.Context, db Database) (*Credentials, error)

Credentials reads the read-only role's basic-auth Secret (dbgrants.SecretName) and the cluster CA (CASecretName, key ca.crt) of db. Only those two Secrets are read, and only those two keys are kept.

func (*Client) CredentialsFor added in v1.0.9

func (c *Client) CredentialsFor(ctx context.Context, db Database, role Role) (*Credentials, error)

CredentialsFor is Client.Credentials for the named role: the Secret read is the role's own (<db>-forge-ro or <db>-forge-dash) and nothing else of the roles', so a caller that may reach only dashboards never holds forge_ro's password. A missing or empty Secret wraps the role's own error (ErrNoReadOnlyCredentials, ErrNoDashboardCredentials).

func (*Client) Dial

func (c *Client) Dial(ctx context.Context, t Target) (net.Conn, error)

Dial opens one connection to a ready pod behind t: one resolve, one API server connection, one stream pair. The returned net.Conn honours read and write deadlines, which is what pgx cancels a query with. ctx bounds only the dial; cancelling it later does not close the connection.

func (*Client) DialFunc

func (c *Client) DialFunc(t Target) pgconn.DialFunc

DialFunc is Client.Dial for t as a pgx DialFunc (pgconn.Config.DialFunc). The address pgx passes is ignored: the target decides where the connection goes, the DSN's host only names the certificate to verify.

func (*Client) ExternalConnConfig added in v1.0.12

func (c *Client) ExternalConnConfig(ctx context.Context, x External) (*pgx.ConnConfig, error)

ExternalConnConfig reads x's password from its Secret (that one key of that one Secret, nothing else) and returns a config that dials x through the tunnel, as x.Username. The config holds the password; do not log it.

type Credentials

type Credentials struct {
	// Username is the role's name unless the Secret says otherwise.
	Username string
	Password string
	// CAPEM is the cluster CA certificate (ca.crt of <db>-ca).
	CAPEM []byte
}

Credentials are what a connection as one of the roles needs, read through the same API server the tunnel runs over.

type Database

type Database struct {
	Namespace string
	Name      string
	Instances int32
}

Database is one ManagedDatabase: the CNPG Cluster's name, the namespace it runs in and how many instances it has (which picks -ro or -rw).

func (Database) Target

func (d Database) Target() Target

Target is the Service a read of this database goes to.

type External added in v1.0.12

type External struct {
	Namespace string
	// Service fronts the database; the tunnel reaches its Postgres port
	// (PostgresPort) or, when it has one port, that one (a pooler's 6432).
	Service  string
	Database string
	Username string
	// PasswordSecret is a Secret in Namespace; PasswordKey the key of the
	// password in it ("password" when empty).
	PasswordSecret, PasswordKey string
	// SSLMode is SSLModeRequire (encrypted, not verified: the tunnel ends at
	// a pod, not at a name a certificate carries) or SSLModeDisable. Empty is
	// SSLModeRequire.
	SSLMode string
}

External is a Postgres forge does not run (a managed cloud database, one behind a pooler) that a Service in a namespace fronts: the declaration forge.ExternalDatabase. The tunnel is the same as a ManagedDatabase's; what differs is everything forge cannot know about a database it did not create, so the caller states it: the database to open, the role to sign in as and the Secret holding its password.

func (External) Target added in v1.0.12

func (x External) Target() Target

Target is the Service a connection to x lands behind.

type PodDialer

type PodDialer interface {
	// DialPod returns a stream to port inside the pod. Closing it tears the
	// forward down. ctx bounds the setup only; the stream outlives it.
	DialPod(ctx context.Context, namespace, pod string, port int) (io.ReadWriteCloser, error)
}

PodDialer opens one stream to a port of a pod. The default speaks the Kubernetes port-forward protocol through the API server; a test or a different tunnel substitutes its own through NewWithClientset.

type Role added in v1.0.9

type Role int

Role is which Postgres role a connection is made as. The zero value is the read-only role.

const (
	// RoleReadOnly is dbgrants.RoleName (forge_ro), secret <db>-forge-ro.
	RoleReadOnly Role = iota
	// RoleDashboard is dbgrants.DashboardRoleName (forge_dash), secret
	// <db>-forge-dash. It reads only forge:dashboard tables.
	RoleDashboard
)

type Target

type Target struct {
	Namespace string
	Service   string
}

Target is a Service whose ready pods a connection can land on.

func (Target) Host

func (t Target) Host() string

Host is the in-cluster name a TLS certificate is verified against, and the host a DSN built for this target carries: <service>.<namespace>.svc.

Jump to

Keyboard shortcuts

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