Skip to content

package s3

import "github.com/cloudboss/unobin/pkg/state/s3"

Package s3 stores state snapshots in an S3 bucket. The layout under the configured prefix mirrors the local store's directory layout, one object per snapshot plus a current pointer and a lock marker:

[<prefix>/]<factory>/<stack>/
  current             // Object holding the rev of the current snapshot.
  lock                // Lock marker with holder info, present while held.
  snapshots/
    <rev>.json.enc    // rev is an RFC3339Nano timestamp.

Exclusion relies on S3 conditional writes: the lock marker and each snapshot are created with If-None-Match, so a concurrent create loses with a precondition failure instead of clobbering. Stores without conditional-write support cannot hold the lock safely and fail at Lock with the store's own error.

Types

type Store

type Store struct {
    Bucket   string
    Prefix   string
    KMSKeyID string
    // contains filtered or unexported fields
}

Store reads and writes snapshots under a per-stack key prefix in one bucket. KMSKeyID, when set, requests SSE-KMS with that key on every object written, the lock marker and current pointer included, so bucket policies that deny unencrypted puts hold.

func NewStore

func NewStore(
    client *s3.Client,
    bucket, prefix, kmsKeyID, factory, stack string,
    enc sdkencrypt.Encrypter,
) (*Store, error)

NewStore returns an Store for the given factory and stack in bucket, with all objects under prefix when it is not empty. The encrypter is required, but a pass-through (encrypters.Noop) can be passed for tests.

func (*Store) Current

func (s *Store) Current() (*sdkstate.Snapshot, error)

Current returns the snapshot named by the current pointer. Returns sdkstate.ErrNoCurrent when no snapshot has been written yet.

func (*Store) CurrentRev

func (s *Store) CurrentRev() (string, error)

CurrentRev returns the rev the current pointer names, or sdkstate.ErrNoCurrent.

func (*Store) Delete

func (s *Store) Delete(rev string) error

Delete removes the snapshot with the given rev. Removing a rev that does not exist is not an error.

func (*Store) ForceUnlock

func (s *Store) ForceUnlock() error

ForceUnlock removes the lock marker without checking who holds it. Operators run this to recover after a leaked lock and must ensure no concurrent run is in progress.

func (*Store) Get

func (s *Store) Get(rev string) (*sdkstate.Snapshot, error)

Get returns the snapshot with the given rev.

func (*Store) List

func (s *Store) List() ([]string, error)

List returns the revs of every stored snapshot in chronological order. S3 lists keys lexically, which is chronological for RFC3339Nano revs.

func (*Store) Lock

func (s *Store) Lock(ctx context.Context) (sdkstate.Lock, error)

Lock acquires the stack's exclusive lock by creating the lock marker with If-None-Match. Lock blocks until the create wins or ctx is canceled; while blocked it polls on the same cadence as the local store. A canceled wait names the holder in its error.

func (*Store) SetCurrent

func (s *Store) SetCurrent(rev string) error

SetCurrent atomically points "current" at the named rev. The snapshot must already exist.

func (*Store) Stack

func (s *Store) Stack() string

Stack returns the stack name this store was constructed for. Required by the Backend interface.

func (*Store) Write

func (s *Store) Write(snap *sdkstate.Snapshot) (string, error)

Write commits snap to the bucket and returns its rev. The caller advances the current pointer with SetCurrent. Each rev starts as an RFC3339Nano timestamp; the snapshot object is created with If-None-Match, and on a precondition failure (two writes sharing the same nanosecond) a numeric suffix is appended until the create wins, so uniqueness does not depend on the clock advancing between writes.