feat(auth): coordinate canonical projection publication
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user