350 lines
10 KiB
Go
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),
|
|
}
|
|
}
|