quarktenant

package
v1.1.2 Latest Latest
Warning

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

Go to latest
Published: Jun 10, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package quarktenant is the thin CLI wrapper that turns a quark.Client + a set of registered Go models into a tenant row-level-security install workflow. The pattern mirrors [quarkmigrate]: a small library the user embeds in a tiny `main.go`, no shipped binary that knows about user models.

Usage:

// myapp/tenant/main.go
package main
import (
    "context"
    "os"
    "github.com/jcsvwinston/quark"
    "github.com/jcsvwinston/quark/quarktenant"
    "myapp/models"
)
func main() {
    client, err := quark.New(os.Getenv("QUARK_DIALECT"), os.Getenv("QUARK_DSN"))
    if err != nil { os.Exit(2) }
    defer client.Close()
    _ = client.RegisterModel(&models.Order{}, &models.Invoice{})
    os.Exit(quarktenant.Run(context.Background(), os.Args[1:], client))
}

Then in CI / Makefile:

go run ./tenant install-rls-policies --dry-run    # print DDL
go run ./tenant install-rls-policies              # actually install

Only PostgreSQL is supported. Other dialects return quark.ErrUnsupportedFeature from InstallRLSPolicies.

Index

Constants

View Source
const (
	ExitSuccess = 0
	ExitError   = 2
)

Exit codes returned by Run:

0 — success: DDL printed (dry-run) or applied successfully.
2 — operational error (unsupported dialect, generation failure,
    apply failure, unknown action).

Variables

View Source
var ErrInvalidCast = errors.New("quarktenant: invalid SQL cast")

ErrInvalidCast is returned by InstallRLSPolicies when the TenantColumnSQLCast value (or the `--cast` CLI flag) does not match a single PostgreSQL type token. See [castIdentifier] for the allowed shape. This guard exists to prevent the policy SQL from being extended with arbitrary statements via the cast input.

View Source
var ErrNoRegisteredModels = errors.New("quarktenant: no models registered on the Client")

ErrNoRegisteredModels is returned when InstallRLSPolicies is invoked on a Client that has no models registered. Register models with quark.Client.RegisterModel before calling.

View Source
var ErrNoTenantColumn = errors.New("quarktenant: model is missing the configured TenantColumn")

ErrNoTenantColumn is returned by InstallRLSPolicies when a registered model does not declare the configured TenantColumn. Wrap your model with the column or skip it from registration.

Functions

func InstallRLSPolicies

func InstallRLSPolicies(ctx context.Context, client *quark.Client, opts InstallOptions) ([]string, error)

InstallRLSPolicies generates the policy DDL (and, when opts.DryRun is false, applies it) for every model registered on the Client. Returns the rendered DDL statements in order so the caller can print, log, or pipe them — regardless of DryRun.

PostgreSQL-only. Returns quark.ErrUnsupportedFeature wrapped with the dialect name on any other engine.

Apply path: a single PostgreSQL transaction wraps every statement across every registered model. PG supports transactional DDL — `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, `CREATE POLICY`, the `FORCE` variant — so a failure mid-stream rolls back the whole install. On success the migration lock is released and all policies are visible together.

The apply path drops down to [Client.Raw] to manage the transaction directly; it does not pass through SQLGuard because the statements are generated from registered model metadata, not from caller input. The cast token in the policy USING/WITH CHECK expression is validated separately via [castIdentifier] (see ErrInvalidCast).

Idempotence: the policy name is deterministic (<table>_tenant_isolation). Re-running InstallRLSPolicies on a table that already has the policy installed will fail at apply time with a PostgreSQL duplicate-object error (SQLSTATE 42710). Callers who want idempotent install should DROP the policy first or guard the call with their own existence check.

func Run

func Run(ctx context.Context, args []string, client *quark.Client) int

Run is the CLI entry point. Pass it the raw args slice (typically `os.Args[1:]`) and a quark.Client with models registered. Returns an exit code suitable for `os.Exit(...)`.

Flags consumed (in addition to the action positional):

--dry-run                  print DDL, do not apply
--tenant-col <name>        column name (default: tenant_id)
--native-rls-var <name>    PG setting name (default: app.tenant_id)
--cast <sql>               cast appended after current_setting (default: text)
--no-force-rls             omit ALTER TABLE ... FORCE ROW LEVEL SECURITY
--lock-name <name>         migration lock name (default: quark_install_rls_policies)
--lock-timeout <duration>  migration lock acquire timeout (default: 30s)

Output goes to stdout for DDL and stderr for errors / status; the destinations are overrideable with the RunWithIO variant for tests.

func RunWithIO

func RunWithIO(ctx context.Context, args []string, client *quark.Client, stdout, stderr io.Writer) int

RunWithIO is Run with explicit io.Writer destinations. Used by the test suite to capture stdout/stderr without touching globals.

Types

type Action

type Action string

Action is the subcommand quarktenant is asked to run. The F5-3 scope ships a single action; the type is kept open-ended so the follow-up F5-N items (drop, list, audit) can extend it without breaking the CLI shape.

const (
	// ActionInstallRLSPolicies generates the RLS policy DDL for
	// every model registered on the [quark.Client]. With --dry-run,
	// the DDL is printed to stdout and no database changes are made.
	// Without --dry-run, the DDL is applied under a distributed
	// migration lock so concurrent installers are serialised.
	ActionInstallRLSPolicies Action = "install-rls-policies"
)

func ParseAction

func ParseAction(arg string) (Action, error)

ParseAction returns the Action for the given CLI argument. The empty string and any unknown value return an error so the caller can map to ExitError and surface the failure with usage text.

type InstallOptions

type InstallOptions struct {
	// TenantColumn is the column name used in policies and inserted
	// into the WITH CHECK clause. Default: "tenant_id". Must exist
	// on every registered model (validated at generation time).
	TenantColumn string

	// NativeRLSVar is the PostgreSQL session variable referenced by
	// the policy via current_setting(NativeRLSVar, true). Must match
	// the value the [quark.TenantRouter] uses at runtime
	// (TenantConfig.NativeRLSVar). Default: "app.tenant_id".
	NativeRLSVar string

	// ForceRLS toggles `ALTER TABLE <t> FORCE ROW LEVEL SECURITY`
	// alongside `ENABLE ROW LEVEL SECURITY`. Without FORCE the table
	// owner can bypass the policy — usually the application role IS
	// the owner, so leaving FORCE off makes the policy decorative.
	// Default: true.
	ForceRLS bool

	// DryRun, when true, generates the DDL but does not apply it.
	// InstallRLSPolicies still returns the rendered statements so the
	// caller can print them or pipe them.
	DryRun bool

	// LockTimeout caps how long [InstallRLSPolicies] will wait for
	// the distributed migration lock when DryRun is false. Default:
	// 30 seconds. The lock prevents two installers from racing on
	// the same database.
	LockTimeout time.Duration

	// LockName is the [quark.Client.AcquireMigrationLock] name used
	// for the install run. Default: "quark_install_rls_policies".
	// Override only if you need to coordinate with custom tooling
	// that already takes the default migration lock.
	LockName string

	// TenantColumnSQLCast is the explicit PostgreSQL cast appended
	// to `current_setting(NativeRLSVar, true)` inside the policy
	// USING/WITH CHECK clauses, e.g. "::text", "::uuid",
	// "::bigint". When empty, InstallRLSPolicies infers `::text`
	// for string columns and falls back to `::text` otherwise —
	// callers using uuid or bigint tenant IDs MUST set this
	// explicitly. The cast is emitted verbatim, surrounded by the
	// leading "::" if the caller did not include one, so both
	// "uuid" and "::uuid" work.
	TenantColumnSQLCast string
}

InstallOptions configures policy generation for InstallRLSPolicies.

The zero value is unusable — call DefaultInstallOptions to obtain a populated struct and override fields as needed.

func DefaultInstallOptions

func DefaultInstallOptions() InstallOptions

DefaultInstallOptions returns the populated defaults the F5-2 runtime expects. Callers typically tweak DryRun and (rarely) TenantColumn / NativeRLSVar before passing the struct to InstallRLSPolicies.

Jump to

Keyboard shortcuts

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