Files
ThothII/tools/tht/internal/authconfig/projection_transaction.go
T

350 lines
10 KiB
Go

package authconfig
import (
"context"
"errors"
"sync"
"github.com/aritmolab/thothii/tools/tht/internal/authprojection"
)
const transactionLockFileName = ".auth-transaction.lock"
var (
errProjectionMutation = errors.New("authentication projection mutation failed")
errProjectionPublish = errors.New("authentication projection publication failed")
errProjectionCleanup = errors.New("authentication projection cleanup failed")
errProjectionIntegrity = errors.New("authentication projection coordination failed")
)
var runtimeProjectionBegin = authprojection.BeginContext
func setRuntimeProjectionBeginForTest(begin func(context.Context, authprojection.Spec, *authprojection.Snapshot, bool) (*authprojection.Transaction, error)) func() {
previous := runtimeProjectionBegin
runtimeProjectionBegin = begin
return func() { runtimeProjectionBegin = previous }
}
type ProjectionSpec struct {
RuntimeRoot string
UID uint32
GID uint32
}
type ProjectionStatus struct {
State string
Generation string
CanonicalRevision string
Equal bool
}
type ExternalProjectionTransaction struct {
ctx context.Context
canonicalRoot string
spec authprojection.Spec
outerLock *transactionLock
projection *authprojection.Transaction
before authprojection.Snapshot
hasBefore bool
published bool
closed bool
}
type projectionCoordinatorHooks struct {
onOuterLockContention func()
onCanonicalLockContention func()
beforeProjectionBegin func()
verifyCommittedProjection func(authprojection.Status, authprojection.Snapshot) error
}
var projectionCoordinatorHookState struct {
sync.Mutex
hooks projectionCoordinatorHooks
}
func setProjectionCoordinatorHooksForTest(hooks projectionCoordinatorHooks) func() {
projectionCoordinatorHookState.Lock()
previous := projectionCoordinatorHookState.hooks
projectionCoordinatorHookState.hooks = hooks
projectionCoordinatorHookState.Unlock()
return func() {
projectionCoordinatorHookState.Lock()
projectionCoordinatorHookState.hooks = previous
projectionCoordinatorHookState.Unlock()
}
}
func coordinatorHooks() projectionCoordinatorHooks {
projectionCoordinatorHookState.Lock()
defer projectionCoordinatorHookState.Unlock()
return projectionCoordinatorHookState.hooks
}
func RunProjectedMutation(ctx context.Context, canonicalRoot string, spec ProjectionSpec, mutate func() error) (err error) {
if mutate == nil {
return errProjectionMutation
}
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, spec)
if err != nil {
return errProjectionIntegrity
}
defer func() {
if closeErr := transaction.Close(); closeErr != nil {
if err == nil {
err = errProjectionCleanup
} else {
err = errors.Join(err, errProjectionCleanup)
}
}
}()
if mutateErr := mutate(); mutateErr != nil {
if restoreErr := transaction.restorePriorIfCanonicalUnchanged(ctx); restoreErr != nil {
return errors.Join(errProjectionMutation, errProjectionCleanup)
}
return errProjectionMutation
}
if _, publishErr := transaction.publishCanonicalContext(ctx); publishErr != nil {
return errProjectionPublish
}
return nil
}
func PublishProjectedCanonical(ctx context.Context, canonicalRoot string, spec ProjectionSpec) (status ProjectionStatus, err error) {
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, spec)
if err != nil {
return ProjectionStatus{}, errProjectionIntegrity
}
defer func() {
if closeErr := transaction.Close(); closeErr != nil {
if err == nil {
err = errProjectionCleanup
} else {
err = errors.Join(err, errProjectionCleanup)
}
}
}()
return transaction.publishCanonicalContext(ctx)
}
func BeginExternalProjectionTransaction(ctx context.Context, canonicalRoot string, spec ProjectionSpec) (transaction *ExternalProjectionTransaction, err error) {
if ctx == nil {
return nil, errProjectionIntegrity
}
if err := requirePrivateDirectory(canonicalRoot); err != nil {
return nil, errProjectionIntegrity
}
outerLock, err := acquireTransactionLock(ctx, canonicalRoot)
if err != nil {
return nil, errProjectionIntegrity
}
transaction = &ExternalProjectionTransaction{
ctx: ctx,
canonicalRoot: canonicalRoot,
spec: authprojection.Spec{RuntimeRoot: spec.RuntimeRoot, UID: spec.UID, GID: spec.GID},
outerLock: outerLock,
}
created := transaction
defer func() {
if err != nil {
if closeErr := created.closeProjectionAndOuterLock(); closeErr != nil {
err = errors.Join(errProjectionIntegrity, errProjectionCleanup)
} else {
err = errProjectionIntegrity
}
}
}()
if err = transaction.beginProjection(ctx); err != nil {
return nil, err
}
return transaction, nil
}
func (transaction *ExternalProjectionTransaction) PublishCanonical() (ProjectionStatus, error) {
if transaction == nil {
return ProjectionStatus{}, errProjectionIntegrity
}
return transaction.publishCanonicalContext(transaction.ctx)
}
func (transaction *ExternalProjectionTransaction) publishCanonicalContext(ctx context.Context) (ProjectionStatus, error) {
if transaction == nil || transaction.closed {
return ProjectionStatus{}, errProjectionIntegrity
}
if transaction.projection == nil {
if err := transaction.beginProjection(ctx); err != nil {
return ProjectionStatus{}, errProjectionIntegrity
}
}
var published authprojection.Status
var expected authprojection.Snapshot
committed := false
err := transaction.withCanonicalLock(ctx, func() error {
snapshot, err := loadSnapshotBytes(transaction.canonicalRoot)
if err != nil {
return err
}
expected = snapshot
published, err = transaction.projection.Commit(snapshot)
if err != nil {
return err
}
committed = true
if !equalProjection(published, snapshot) {
return errProjectionIntegrity
}
if verify := coordinatorHooks().verifyCommittedProjection; verify != nil {
return verify(published, snapshot)
}
return nil
})
if err != nil {
if committed {
if blockErr := transaction.ensureBlocked(); blockErr != nil {
return ProjectionStatus{}, errors.Join(errProjectionPublish, errProjectionCleanup)
}
}
return ProjectionStatus{}, errProjectionPublish
}
transaction.published = true
if err := transaction.releaseProjection(); err != nil {
if blockErr := transaction.ensureBlocked(); blockErr != nil {
return ProjectionStatus{}, errors.Join(errProjectionCleanup, errProjectionIntegrity)
}
return ProjectionStatus{}, errProjectionCleanup
}
return projectionStatus(published, expected), nil
}
func (transaction *ExternalProjectionTransaction) RestorePriorIfCanonicalUnchanged() error {
if transaction == nil {
return errProjectionIntegrity
}
return transaction.restorePriorIfCanonicalUnchanged(transaction.ctx)
}
func (transaction *ExternalProjectionTransaction) restorePriorIfCanonicalUnchanged(ctx context.Context) error {
if transaction == nil || transaction.closed || transaction.projection == nil || !transaction.hasBefore {
return errProjectionIntegrity
}
err := transaction.withCanonicalLock(ctx, func() error {
current, err := loadSnapshotBytes(transaction.canonicalRoot)
if err != nil {
return err
}
if current.Generation != transaction.before.Generation {
return errProjectionIntegrity
}
return transaction.projection.RestoreIfUnchanged(current)
})
if err != nil {
return errProjectionIntegrity
}
return transaction.releaseProjection()
}
func (transaction *ExternalProjectionTransaction) Close() (err error) {
if transaction == nil || transaction.closed {
return nil
}
defer func() {
transaction.closed = true
err = errors.Join(err, transaction.closeProjectionAndOuterLock())
}()
if transaction.projection == nil && transaction.published {
if beginErr := transaction.beginProjection(context.Background()); beginErr != nil {
return errProjectionIntegrity
}
if restoreErr := transaction.restorePriorIfCanonicalUnchanged(context.Background()); restoreErr != nil {
return errProjectionIntegrity
}
}
return nil
}
func (transaction *ExternalProjectionTransaction) beginProjection(ctx context.Context) error {
status, err := authprojection.Inspect(transaction.spec)
var before *authprojection.Snapshot
requireReadyMatch := false
if err == nil {
captured := status.Snapshot
before = &captured
requireReadyMatch = true
}
if hook := coordinatorHooks().beforeProjectionBegin; hook != nil {
hook()
}
projection, err := runtimeProjectionBegin(ctx, transaction.spec, before, requireReadyMatch)
if err != nil {
return errProjectionIntegrity
}
transaction.projection = projection
transaction.hasBefore = before != nil
if before != nil {
transaction.before = *before
} else {
transaction.before = authprojection.Snapshot{}
}
return nil
}
func (transaction *ExternalProjectionTransaction) ensureBlocked() error {
if transaction.projection != nil {
if err := transaction.releaseProjection(); err != nil {
return err
}
}
return transaction.beginProjection(context.Background())
}
func (transaction *ExternalProjectionTransaction) releaseProjection() error {
if transaction.projection == nil {
return nil
}
projection := transaction.projection
transaction.projection = nil
if err := projection.Close(); err != nil {
return errProjectionIntegrity
}
return nil
}
func (transaction *ExternalProjectionTransaction) closeProjectionAndOuterLock() error {
var err error
if closeErr := transaction.releaseProjection(); closeErr != nil {
err = errors.Join(err, closeErr)
}
if transaction.outerLock != nil {
if unlockErr := transaction.outerLock.Close(); unlockErr != nil {
err = errors.Join(err, errProjectionCleanup)
}
transaction.outerLock = nil
}
return err
}
func (transaction *ExternalProjectionTransaction) withCanonicalLock(ctx context.Context, callback func() error) (err error) {
lock, err := acquireLockContext(ctx, transaction.canonicalRoot)
if err != nil {
return errProjectionIntegrity
}
defer func() {
if unlockErr := lock.Unlock(); unlockErr != nil {
err = errors.Join(err, errProjectionCleanup)
}
}()
return callback()
}
func equalProjection(status authprojection.Status, snapshot authprojection.Snapshot) bool {
return status.Selector.State == "ready" && status.Snapshot.Generation == snapshot.Generation && status.Snapshot.CanonicalRevision == snapshot.CanonicalRevision
}
func projectionStatus(status authprojection.Status, expected authprojection.Snapshot) ProjectionStatus {
return ProjectionStatus{
State: status.Selector.State,
Generation: status.Snapshot.Generation,
CanonicalRevision: status.Snapshot.CanonicalRevision,
Equal: equalProjection(status, expected),
}
}