feat(auth): coordinate canonical projection publication
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
//go:build linux
|
||||
|
||||
package authconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/authprojection"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
|
||||
)
|
||||
|
||||
func TestBeginExternalTransactionReleasesOuterLockAfterRuntimeCancellation(t *testing.T) {
|
||||
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
|
||||
spec := testProjectionSpec(t)
|
||||
raw, err := authprojection.Begin(toRuntimeSpec(spec), nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if raw != nil {
|
||||
_ = raw.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
admitted := make(chan struct{})
|
||||
restore := setRuntimeProjectionBeginForTest(func(ctx context.Context, runtimeSpec authprojection.Spec, before *authprojection.Snapshot, requireReadyMatch bool) (*authprojection.Transaction, error) {
|
||||
close(admitted)
|
||||
return authprojection.BeginContext(ctx, runtimeSpec, before, requireReadyMatch)
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, spec)
|
||||
if err == nil {
|
||||
err = transaction.Close()
|
||||
}
|
||||
result <- err
|
||||
}()
|
||||
<-admitted
|
||||
cancel()
|
||||
select {
|
||||
case err := <-result:
|
||||
if err == nil {
|
||||
t.Fatal("BeginExternalProjectionTransaction() unexpectedly succeeded after cancellation")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("BeginExternalProjectionTransaction() did not return after runtime cancellation")
|
||||
}
|
||||
restore()
|
||||
|
||||
if err := raw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw = nil
|
||||
next, err := BeginExternalProjectionTransaction(context.Background(), canonicalRoot, spec)
|
||||
if err != nil {
|
||||
t.Fatalf("outer lock remained held after runtime cancellation: %v", err)
|
||||
}
|
||||
if err := next.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalPublishContextReturnsWhenCanonicalLockIsHeld(t *testing.T) {
|
||||
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, testProjectionSpec(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = transaction.Close() })
|
||||
canonicalLock, err := acquireLock(canonicalRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = canonicalLock.Unlock() })
|
||||
contended := make(chan struct{}, 1)
|
||||
reblocked := make(chan struct{}, 1)
|
||||
restore := setProjectionCoordinatorHooksForTest(projectionCoordinatorHooks{
|
||||
onCanonicalLockContention: func() { contended <- struct{}{} },
|
||||
beforeProjectionBegin: func() { reblocked <- struct{}{} },
|
||||
})
|
||||
t.Cleanup(restore)
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := transaction.PublishCanonical()
|
||||
result <- err
|
||||
}()
|
||||
<-contended
|
||||
if _, err := authprojection.Inspect(toRuntimeSpec(testProjectionSpecFromTransaction(transaction))); !errors.Is(err, authprojection.ErrBlocked) {
|
||||
t.Fatalf("Inspect() error = %v, want blocked while waiting for canonical lock", err)
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case <-reblocked:
|
||||
t.Fatal("publishCanonicalContext() re-blocked even though Commit was never reached")
|
||||
case err := <-result:
|
||||
if !errors.Is(err, errProjectionPublish) {
|
||||
t.Fatalf("publishCanonicalContext() error = %v, want sanitized publish failure", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("publishCanonicalContext() did not return after cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalRestoreContextReturnsWhenCanonicalLockIsHeld(t *testing.T) {
|
||||
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
|
||||
spec := testProjectionSpec(t)
|
||||
publishCanonical(t, canonicalRoot, spec)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, spec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = transaction.Close() })
|
||||
canonicalLock, err := acquireLock(canonicalRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = canonicalLock.Unlock() })
|
||||
contended := make(chan struct{}, 1)
|
||||
restoreHooks := setProjectionCoordinatorHooksForTest(projectionCoordinatorHooks{
|
||||
onCanonicalLockContention: func() { contended <- struct{}{} },
|
||||
})
|
||||
t.Cleanup(restoreHooks)
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- transaction.RestorePriorIfCanonicalUnchanged() }()
|
||||
<-contended
|
||||
if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) {
|
||||
t.Fatalf("Inspect() error = %v, want blocked while waiting for canonical lock", err)
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-result:
|
||||
if !errors.Is(err, errProjectionIntegrity) {
|
||||
t.Fatalf("RestorePriorIfCanonicalUnchanged() error = %v, want sanitized integrity failure", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("RestorePriorIfCanonicalUnchanged() did not return after cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireTransactionLockRejectsSwapBetweenOpenAndRevalidation(t *testing.T) {
|
||||
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
|
||||
opened := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var firstOpen atomic.Bool
|
||||
restore := setProjectionLockHooksForTest(projectionLockHooks{afterOpen: func() {
|
||||
if firstOpen.CompareAndSwap(false, true) {
|
||||
close(opened)
|
||||
<-release
|
||||
}
|
||||
}})
|
||||
t.Cleanup(restore)
|
||||
first := make(chan struct {
|
||||
lock *transactionLock
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
lock, err := acquireTransactionLock(context.Background(), canonicalRoot)
|
||||
first <- struct {
|
||||
lock *transactionLock
|
||||
err error
|
||||
}{lock, err}
|
||||
}()
|
||||
<-opened
|
||||
replacement := filepath.Join(canonicalRoot, ".replacement-lock")
|
||||
if err := safeio.WriteCanonicalNewFile(replacement, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Rename(replacement, filepath.Join(canonicalRoot, transactionLockFileName)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := acquireTransactionLock(context.Background(), canonicalRoot)
|
||||
if err != nil {
|
||||
t.Fatalf("replacement-domain acquisition error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = second.Close() })
|
||||
close(release)
|
||||
firstResult := <-first
|
||||
if firstResult.lock != nil || !errors.Is(firstResult.err, errProjectionIntegrity) {
|
||||
t.Fatalf("swapped original acquisition = %#v, %v; want no old-domain lock", firstResult.lock, firstResult.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireTransactionLockRejectsUnsafeMetadata(t *testing.T) {
|
||||
for _, fixture := range []struct {
|
||||
name string
|
||||
arrange func(*testing.T, string)
|
||||
}{
|
||||
{
|
||||
name: "symlink",
|
||||
arrange: func(t *testing.T, path string) {
|
||||
target := path + ".target"
|
||||
if err := safeio.WriteCanonicalNewFile(target, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testsupport.SymlinkOrSkip(t, target, path)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hardlink",
|
||||
arrange: func(t *testing.T, path string) {
|
||||
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Link(path, path+".linked"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-0600 mode",
|
||||
arrange: func(t *testing.T, path string) {
|
||||
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(path, 0o640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong owner",
|
||||
arrange: func(t *testing.T, path string) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("requires root to create a wrong numeric owner")
|
||||
}
|
||||
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chown(path, 10001, 10001); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(fixture.name, func(t *testing.T) {
|
||||
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
|
||||
fixture.arrange(t, filepath.Join(canonicalRoot, transactionLockFileName))
|
||||
lock, err := acquireTransactionLock(context.Background(), canonicalRoot)
|
||||
if lock != nil || !errors.Is(err, errProjectionIntegrity) {
|
||||
t.Fatalf("acquireTransactionLock() = %#v, %v; want unsafe metadata rejection", lock, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireTransactionLockAcceptsSafeCreatorRace(t *testing.T) {
|
||||
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
|
||||
paused := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var firstCreate atomic.Bool
|
||||
restore := setProjectionLockHooksForTest(projectionLockHooks{beforeCreate: func() {
|
||||
if firstCreate.CompareAndSwap(false, true) {
|
||||
close(paused)
|
||||
<-release
|
||||
}
|
||||
}})
|
||||
t.Cleanup(restore)
|
||||
first := make(chan struct {
|
||||
lock *transactionLock
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
lock, err := acquireTransactionLock(context.Background(), canonicalRoot)
|
||||
first <- struct {
|
||||
lock *transactionLock
|
||||
err error
|
||||
}{lock, err}
|
||||
}()
|
||||
<-paused
|
||||
second, err := acquireTransactionLock(context.Background(), canonicalRoot)
|
||||
if err != nil {
|
||||
t.Fatalf("creator-race winner error = %v", err)
|
||||
}
|
||||
if err := second.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
close(release)
|
||||
result := <-first
|
||||
if result.err != nil || result.lock == nil {
|
||||
t.Fatalf("creator-race loser = %#v, %v; want same validated lock domain", result.lock, result.err)
|
||||
}
|
||||
if err := result.lock.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testProjectionSpecFromTransaction(transaction *ExternalProjectionTransaction) ProjectionSpec {
|
||||
return ProjectionSpec{RuntimeRoot: transaction.spec.RuntimeRoot, UID: transaction.spec.UID, GID: transaction.spec.GID}
|
||||
}
|
||||
Reference in New Issue
Block a user