Skip to content

Conversation

@jzelinskie
Copy link
Member

@jzelinskie jzelinskie commented Dec 11, 2025

Description

This PR adds backup support for systems without the bulk export. This is important for allowing folks to perform backups against AuthZed Serverless (as well as old SpiceDB deployments or ones where their API access is restricted).

I went about this change by refactoring a bit of logic in the create backup command and leaning into the Encoder abstraction in the backupfmt package. Now the logic for persisting to a file and tracking progress on the CLI are isolated and implemented as their own Encoders.

I didn't try to deduplicate any logic between the bulk export and legacy backup conditional because I expect us to be able to just delete the legacy backup block once AuthZed Serverless is fully sunset.

I did also change how we storing our cursor in the lock file. Previously we held a file descriptor open and kept rewriting it and now I create a new tempfile and atomically replace the lockfile when there's an update; this strategy is more robust to filesystem errors and means we have less bookkeeping.

As a follow-up, I also refactored how we were rewriting legacy prefixes, filtering prefixes, and added support for replacing prefixes by creating a Rewriter interface. This ensures the flags are also the same across commands.

Testing

I've manually ran this against some permission systems in AuthZed Serverless and written some basic unit tests for the encoder logic that was refactored.

@jzelinskie jzelinskie marked this pull request as draft December 11, 2025 19:32
@codecov-commenter
Copy link

codecov-commenter commented Dec 12, 2025

Codecov Report

❌ Patch coverage is 51.49701% with 243 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.77%. Comparing base (7ed5ab0) to head (eeb9e9e).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
internal/cmd/backup.go 41.10% 87 Missing and 9 partials ⚠️
pkg/backupformat/encoder.go 47.94% 67 Missing and 9 partials ⚠️
pkg/backupformat/rewriter.go 71.94% 30 Missing and 9 partials ⚠️
pkg/backupformat/decoder.go 26.92% 19 Missing ⚠️
pkg/backupformat/redaction.go 52.94% 4 Missing and 4 partials ⚠️
internal/testing/test_helpers.go 0.00% 3 Missing ⚠️
internal/cmd/backup_restorer.go 71.42% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #597      +/-   ##
==========================================
+ Coverage   39.28%   40.77%   +1.49%     
==========================================
  Files          37       38       +1     
  Lines        5448     4959     -489     
==========================================
- Hits         2140     2022     -118     
+ Misses       3063     2687     -376     
- Partials      245      250       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jzelinskie jzelinskie marked this pull request as ready for review December 12, 2025 01:41
@jzelinskie jzelinskie force-pushed the backup-failover branch 2 times, most recently from 9bd2ad4 to 100c8df Compare December 16, 2025 01:14
tstirrat15
tstirrat15 previously approved these changes Dec 17, 2025
Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me; I'd also like to get maria's eyes on it.

oneConflictError = []error{errConflict}
)

func TestRestorerFiltered(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this test need to be broken out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually going to break out the filtered vs non-filtered and just totally missed that there's 1 more test case there that uses a filter.

We have lots of duplicated tests that mostly just test filtering. I think it makes sense to try and tease them all out and just unit test the Rewriter exhaustively.

require.NotNil(t, enc.enc)
}

func TestOcfEncoder_Append(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have similar tests that test OcfFileEncoder.Append()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm working on a follow up PR that pulls the rewriter into the encoder/decoder and tests each individually. That should help clean up a lot of these tests

Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, but I'll get Maria's eyes on as well.

func (d *OcfDecoder) Next() (*v1.Relationship, error) {
if !d.dec.HasNext() {
return nil, nil
return nil, io.EOF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that is weird


// OcfEncoder implements `Encoder` by formatting data in the AVRO OCF format.
type OcfEncoder struct {
w io.Writer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


// NewRedactor creates a new redactor that will redact the data as it is written.
func NewRedactor(dec *Decoder, w io.Writer, opts RedactionOptions) (*Redactor, error) {
func NewRedactor(dec Decoder, w io.Writer, opts RedactionOptions) (*Redactor, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was the pointer change necessary here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decoder is an interface now and before that was a pointer to a concrete implementation

@miparnisari miparnisari changed the title backup systems without bulk export feat: add ability to backup systems that don't expose BulkExport API Dec 20, 2025
@miparnisari miparnisari enabled auto-merge (squash) December 20, 2025 00:41
@miparnisari miparnisari merged commit 9a2d5e3 into authzed:main Dec 20, 2025
12 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Dec 20, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants