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
- Variables
- func CASecretName(name string) string
- func ConnConfigForAddr(db Database, creds *Credentials, addr, sslmode string) (*pgx.ConnConfig, error)
- func PolicyRules(databases ...string) []rbacv1.PolicyRule
- func PolicyRulesFor(role Role, databases ...string) []rbacv1.PolicyRule
- func ServerNameForAddr(db Database) string
- func ServiceName(name string, instances int32) string
- type Client
- func (c *Client) ConnConfig(ctx context.Context, db Database) (*pgx.ConnConfig, error)
- func (c *Client) ConnConfigFor(db Database, creds *Credentials) (*pgx.ConnConfig, error)
- func (c *Client) Credentials(ctx context.Context, db Database) (*Credentials, error)
- func (c *Client) CredentialsFor(ctx context.Context, db Database, role Role) (*Credentials, error)
- func (c *Client) Dial(ctx context.Context, t Target) (net.Conn, error)
- func (c *Client) DialFunc(t Target) pgconn.DialFunc
- func (c *Client) ExternalConnConfig(ctx context.Context, x External) (*pgx.ConnConfig, error)
- type Credentials
- type Database
- type External
- type PodDialer
- type Role
- type Target
Constants ¶
const ( SSLModeDisable = "disable" SSLModeRequire = "require" SSLModeVerifyFull = "verify-full" )
SSL modes ConnConfigForAddr speaks. The empty string means SSLModeVerifyFull.
const PostgresPort = 5432
PostgresPort is the port a CNPG instance serves on.
Variables ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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
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 ¶
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 ¶
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
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 ¶
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).
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.
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.