Skip to content

Latest commit

 

History

History
373 lines (292 loc) · 11.6 KB

File metadata and controls

373 lines (292 loc) · 11.6 KB

PBMate SDK

A Go client library for Percona Backup for MongoDB (PBM).

The SDK wraps PBM's internal packages behind stable, domain-typed interfaces. PBM internals can change freely — the SDK's conversion layer absorbs the changes, and consumer code doesn't break.

import sdk "github.com/jcechace/pbmate/sdk/v2"

Quick Start

// Connect to a PBM-configured MongoDB cluster.
client, err := sdk.NewClient(ctx, sdk.WithMongoURI("mongodb://localhost:27017"))
if err != nil {
    log.Fatal(err)
}
defer client.Close(ctx)

// List the 5 most recent backups.
backups, err := client.Backups.List(ctx, sdk.ListBackupsOptions{Limit: 5})
for _, bk := range backups {
    fmt.Printf("%-30s  %-12s  %s\n", bk.Name, bk.Type, bk.Status)
}

// Start a logical backup and wait for it.
result, err := client.Backups.Start(ctx, sdk.StartLogicalBackup{})
if err != nil {
    log.Fatal(err)
}
bk, err := result.Wait(ctx, sdk.BackupWaitOptions{
    OnProgress: func(b *sdk.Backup) {
        fmt.Printf("  %s elapsed...\n", b.Elapsed().Truncate(time.Second))
    },
})
fmt.Printf("backup %s finished: %s (%s)\n", bk.Name, bk.Status, bk.Duration())

Elapsed() returns live elapsed time for in-progress operations and the final duration for completed ones. Duration() returns zero until the operation reaches a terminal status.

Services

The client exposes domain-specific services as interface-typed fields:

Service Field Purpose
BackupService client.Backups List, get, start, wait, delete, cancel backups; pre-delete safety check
RestoreService client.Restores List, get, start, wait for restores
ConfigService client.Config Read/write configuration and storage profiles
ClusterService client.Cluster Cluster topology, agent status, running operations, lock checks
PITRService client.PITR PITR status, oplog timelines, enable/disable, base backup filtering, chunk deletion
LogService client.Logs Query and stream PBM logs

Every service is an interface — mock any of them in your tests.

Examples

Start an Incremental Backup

// Base backup (starts a new chain).
result, err := client.Backups.Start(ctx, sdk.StartIncrementalBackup{
    Base: true,
})

// Subsequent increment (extends the chain).
result, err := client.Backups.Start(ctx, sdk.StartIncrementalBackup{})

Start a Selective Backup to a Named Profile

profile, _ := sdk.NewConfigName("archive")
result, err := client.Backups.Start(ctx, sdk.StartLogicalBackup{
    ConfigName:    profile,
    Namespaces:    []string{"mydb.*", "analytics.*"},
    UsersAndRoles: true, // include users/roles (requires whole-database namespaces)
    Compression:   sdk.CompressionTypeZSTD,
})

Restore from a Backup

// Snapshot restore.
result, err := client.Restores.Start(ctx, sdk.StartSnapshotRestore{
    BackupName: "2026-02-19T20:28:16Z",
})

// Point-in-time restore.
result, err := client.Restores.Start(ctx, sdk.StartPITRRestore{
    BackupName: "2026-02-19T20:28:16Z",
    Target:     sdk.Timestamp{T: 1740000000},
})

// Wait for completion (only works for logical restores — physical/incremental
// restores return ErrRestoreUnwaitable).
if result.Waitable() {
    restore, err := result.Wait(ctx, sdk.RestoreWaitOptions{})
}

Override Performance Tuning

// Backup with custom parallelism.
numColls := 8
result, err := client.Backups.Start(ctx, sdk.StartLogicalBackup{
    NumParallelColls: &numColls,
})

// Restore with custom parallelism and insertion workers.
colls, workers := 4, 2
result, err := client.Restores.Start(ctx, sdk.StartSnapshotRestore{
    BackupName:          "2026-02-19T20:28:16Z",
    NumParallelColls:    &colls,
    NumInsertionWorkers: &workers,
})

All performance fields are *int or *bool — nil means "use the server-configured default". Set them only when you need to override.

Check Cluster Health

agents, err := client.Cluster.Agents(ctx)
for _, a := range agents {
    status := "ok"
    if a.Stale {
        status = "STALE"
    } else if !a.OK {
        status = fmt.Sprintf("ERROR: %v", a.Errors)
    }
    fmt.Printf("%-20s  %-10s  %s  %s\n", a.Node, a.ReplicaSet, a.Role, status)
}

Get Cluster Time

// Read the current MongoDB cluster time (useful for computing PITR targets).
ts, err := client.Cluster.ClusterTime(ctx)
fmt.Printf("cluster time: T=%d I=%d  (%s)\n", ts.T, ts.I, ts.Time().UTC().Format(time.RFC3339))

Monitor and Toggle PITR

status, _ := client.PITR.Status(ctx)
if status.Enabled && status.Running {
    fmt.Println("PITR is actively slicing oplog")
}

timelines, _ := client.PITR.Timelines(ctx)
for _, tl := range timelines {
    fmt.Printf("restore window: %s - %s\n",
        tl.Start.Time().UTC().Format(time.RFC3339),
        tl.End.Time().UTC().Format(time.RFC3339))
}

// Enable or disable PITR (applies immediately, agents detect via epoch bump).
err := client.PITR.Enable(ctx)
err = client.PITR.Disable(ctx)

Stream Logs

ctx, cancel := context.WithCancel(ctx)
defer cancel()

entries, errs := client.Logs.Follow(ctx, sdk.FollowOptions{
    LogFilter: sdk.LogFilter{Severity: sdk.LogSeverityWarning},
})
for entry := range entries {
    fmt.Printf("[%s] %s: %s\n", entry.Severity, entry.Timestamp.UTC().Format(time.RFC3339), entry.Message)
}
if err := <-errs; err != nil {
    log.Printf("follow ended: %v", err)
}

Look Up by Operation ID

// Get a backup by its PBM operation ID (useful when tracking an in-progress op).
bk, err := client.Backups.GetByOpID(ctx, opid)
if errors.Is(err, sdk.ErrNotFound) {
    fmt.Println("no backup found for that operation ID")
}

// Get a restore by its PBM operation ID.
restore, err := client.Restores.GetByOpID(ctx, opid)

Handle Concurrent Operations

result, err := client.Backups.Start(ctx, sdk.StartLogicalBackup{})
if err != nil {
    var concurrent *sdk.ConcurrentOperationError
    if errors.As(err, &concurrent) {
        fmt.Printf("blocked by %s (opid: %s)\n", concurrent.Type, concurrent.OPID)
        return
    }
    log.Fatal(err)
}

Delete Backups and PITR Chunks

// Delete a single backup by name.
_, err := client.Backups.Delete(ctx, sdk.DeleteBackupByName{
    Name: "2026-02-19T20:28:16Z",
})

// Bulk delete backups older than 30 days.
cutoff := time.Now().Add(-30 * 24 * time.Hour)
_, err := client.Backups.Delete(ctx, sdk.DeleteBackupsBefore{
    OlderThan: cutoff,
    Type:      sdk.BackupTypeLogical,
})

// Delete PITR oplog chunks older than 7 days.
_, err := client.PITR.Delete(ctx, sdk.DeletePITROlderThan{
    Duration: 7 * 24 * time.Hour,
})

Pre-Delete Safety Check

// Check whether a backup can be safely deleted before dispatching.
if err := client.Backups.CanDelete(ctx, bk.Name); err != nil {
    switch {
    case errors.Is(err, sdk.ErrDeleteProtectedByPITR):
        fmt.Println("backup is the last PITR base snapshot, cannot delete")
    case errors.Is(err, sdk.ErrNotChainBase):
        fmt.Println("incremental backup must be deleted from its chain base")
    case errors.Is(err, sdk.ErrBackupInProgress):
        fmt.Println("backup is still running, wait for completion")
    }
    return
}
_, err := client.Backups.Delete(ctx, sdk.DeleteBackupByName{Name: bk.Name})

Configuration YAML Roundtrip

// Read config with real credentials (not masked with "***").
yamlBytes, _ := client.Config.GetYAML(ctx, sdk.WithUnmasked())

// ... edit yamlBytes in a text editor or programmatically ...

// Apply the modified config.
client.Config.SetYAML(ctx, bytes.NewReader(edited))

By default GetYAML() masks credentials for safe display. Use WithUnmasked() when the YAML will be edited and re-applied.

Manage Storage Profiles

// List profiles.
profiles, _ := client.Config.ListProfiles(ctx)
for _, p := range profiles {
    fmt.Printf("%s: %s %s\n", p.Name, p.Storage.Type, p.Storage.Path)
}

// Apply a profile from YAML.
f, _ := os.Open("archive-profile.yaml")
defer f.Close()
_, err := client.Config.SetProfile(ctx, "archive", f)

// Remove a storage profile.
err = client.Config.RemoveProfile(ctx, "archive")

Design

Conversion Boundary

The SDK owns all public types. PBM internal types (backup.BackupMeta, ctrl.Cmd, etc.) are converted to SDK types in *_convert.go files before reaching the public API. When PBM internals change, the conversion layer is updated — consumer code stays stable.

Consumer  <-->  SDK types  <-->  *_convert.go  <-->  PBM internals
                (stable)         (absorbs changes)    (can change freely)

Sealed Command Interfaces

Operations that have distinct variants use sealed interfaces with unexported marker methods. This prevents invalid command construction at compile time:

// StartBackupCommand is sealed — only these three types implement it:
//   - StartLogicalBackup      (has Namespaces, UsersAndRoles, NumParallelColls)
//   - StartPhysicalBackup     (has Compression, CompressionLevel, ConfigName)
//   - StartIncrementalBackup  (has Base field)
//
// You can't mix fields from different strategies or pass an arbitrary struct.
result, err := client.Backups.Start(ctx, sdk.StartLogicalBackup{
    Namespaces: []string{"mydb.mycol"},
})

Other sealed hierarchies: StartRestoreCommand (snapshot vs PITR), DeleteBackupCommand (by name vs before timestamp), DeletePITRCommand, ResyncCommand.

Command Validation

Every command type implements Validate() error. Service methods call Validate() before checking locks or dispatching, so invalid commands fail fast with a clear error — no round-trip to MongoDB needed. Commands with no constraints return nil.

cmd := sdk.StartLogicalBackup{
    UsersAndRoles: true,
    // Namespaces is empty — UsersAndRoles requires a selective operation.
}
if err := cmd.Validate(); err != nil {
    fmt.Println(err) // "start backup: users-and-roles is only valid for selective operations (namespaces must be set)"
}

Value Objects

Enum-like types (Status, BackupType, CompressionType, etc.) use unexported value fields with constructor functions. Invalid values can't be created by external code:

// These work:
bt := sdk.BackupTypeLogical                           // predefined constant
bt, err := sdk.ParseBackupType("logical")             // parse from string
bt, err := sdk.ParseBackupType("garbage")             // returns error

// This doesn't compile — value field is unexported:
// bt := sdk.BackupType{value: "garbage"}

Interface-Based Services

Every service is an interface. This enables mocking in consumer tests without wrapping or code generation:

type myApp struct {
    backups sdk.BackupService  // inject real or mock
}

PBM Compatibility

The SDK wraps PBM's internal Go packages at a pinned version. It is tested against PBM clusters running the same major version. Older or newer PBM clusters generally work — unknown enum values are logged as warnings, not errors — but newly added PBM features won't be available until the SDK dependency is updated.

Requirements

  • Go 1.26+
  • A running MongoDB cluster with PBM agents configured
  • Network access to the MongoDB cluster

See Also