Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changes/unreleased/multi-identity-pending-operations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Added
body: Add private identity-mapping configuration and recovery metadata with inhibited pending intents, exact retries, and conservative reservation retention. Read selected overrides through checked regular-file handles without following replacement symlinks. Reject build publication and generation changes until a pending mapping operation finishes. Mapped execution remains unavailable.
383 changes: 383 additions & 0 deletions internal/deploy/identity_mapping_operations.go

Large diffs are not rendered by default.

726 changes: 726 additions & 0 deletions internal/deploy/identity_mapping_operations_test.go

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions internal/deploy/package_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,17 +620,28 @@ func ReadPackageOverridesV1(deploymentDir string) (PackageOverridesV1, bool, err
if err != nil {
return PackageOverridesV1{}, false, err
}
info, err := os.Lstat(path)
return ReadPackageOverridesFileV1(path)
}

// ReadPackageOverridesFileV1 captures one explicitly selected override file.
// Callers decide whether absence is allowed and bind its environment/declaration.
func ReadPackageOverridesFileV1(path string) (PackageOverridesV1, bool, error) {
file, err := openPackageOverridesFile(path)
if errors.Is(err, fs.ErrNotExist) {
return PackageOverridesV1{}, false, nil
}
if err != nil {
return PackageOverridesV1{}, false, fmt.Errorf("open package overrides: %w", err)
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return PackageOverridesV1{}, false, fmt.Errorf("inspect package overrides: %w", err)
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
if !info.Mode().IsRegular() {
return PackageOverridesV1{}, false, fmt.Errorf("package overrides path must be a regular file: %s", path)
}
content, err := os.ReadFile(path)
content, err := io.ReadAll(file)
if err != nil {
return PackageOverridesV1{}, false, fmt.Errorf("read package overrides: %w", err)
}
Expand Down
18 changes: 18 additions & 0 deletions internal/deploy/package_overrides_open_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build !windows

package deploy

import (
"os"

"golang.org/x/sys/unix"
)

func openPackageOverridesFile(path string) (*os.File, error) {
// Do not follow a replaced symlink or wait for a FIFO writer before Stat.
fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), path), nil
}
118 changes: 118 additions & 0 deletions internal/deploy/package_overrides_open_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//go:build !windows

package deploy

import (
"os"
"path/filepath"
"runtime"
"testing"

"golang.org/x/sys/unix"
)

func TestReadPackageOverridesFileRejectsSpecialFiles(t *testing.T) {
for _, kind := range []string{"symlink", "dangling-symlink", "fifo", "directory"} {
t.Run(kind, func(t *testing.T) {
dir := t.TempDir()
selected := filepath.Join(dir, "selected.yaml")
var err error
switch kind {
case "symlink", "dangling-symlink":
target := filepath.Join(dir, "target.yaml")
if kind == "symlink" {
err = os.WriteFile(target, overrideReadFixture(t, "target"), 0o600)
if err != nil {
t.Fatal(err)
}
}
err = os.Symlink(target, selected)
case "fifo":
err = unix.Mkfifo(selected, 0o600)
case "directory":
err = os.Mkdir(selected, 0o700)
}
if err != nil {
t.Fatal(err)
}
if _, found, err := ReadPackageOverridesFileV1(selected); err == nil || found {
t.Fatalf("special file accepted: found=%v, err=%v", found, err)
}
})
}
}

func TestReadPackageOverridesFileNeverFollowsReplacement(t *testing.T) {
previous := runtime.GOMAXPROCS(2)
defer runtime.GOMAXPROCS(previous)
dir := t.TempDir()
selected := filepath.Join(dir, "selected.yaml")
other := filepath.Join(dir, "other")
regular := filepath.Join(dir, "regular.yaml")
target := filepath.Join(dir, "target.yaml")
if err := os.WriteFile(selected, overrideReadFixture(t, "selected"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(target, overrideReadFixture(t, "forbidden-target"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Link(selected, regular); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, other); err != nil {
t.Fatal(err)
}
stop := make(chan struct{})
done := make(chan error, 1)
go func() {
for {
select {
case <-stop:
done <- nil
return
default:
}
// Each replacement is atomic. Neither regular file's contents change.
for _, replace := range []func() error{
func() error { return os.Rename(other, selected) },
func() error { return os.Link(regular, other) },
func() error { return os.Rename(other, selected) },
func() error { return os.Symlink(target, other) },
} {
if err := replace(); err != nil {
done <- err
return
}
}
}
}()
defer func() {
close(stop)
if err := <-done; err != nil {
t.Fatal(err)
}
}()
reads := 0
for range 16384 {
overrides, found, err := ReadPackageOverridesFileV1(selected)
if err != nil || !found {
continue // Rejected symlink entries are valid race outcomes.
}
reads++
if overrides.Environment.ID != "selected" {
t.Fatalf("followed a replacement symlink: environment=%q", overrides.Environment.ID)
}
}
if reads == 0 {
t.Fatal("race test did not exercise a successful regular-file read")
}
}

func overrideReadFixture(t *testing.T, id string) []byte {
t.Helper()
content, err := EncodePackageOverridesV1(EmptyPackageOverridesV1(id))
if err != nil {
t.Fatal(err)
}
return content
}
24 changes: 24 additions & 0 deletions internal/deploy/package_overrides_open_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build windows

package deploy

import (
"os"

"golang.org/x/sys/windows"
)

func openPackageOverridesFile(path string) (*os.File, error) {
pointer, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
handle, err := windows.CreateFile(pointer, windows.GENERIC_READ,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
nil, windows.OPEN_EXISTING,
windows.FILE_FLAG_OPEN_REPARSE_POINT|windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(handle), path), nil
}
3 changes: 3 additions & 0 deletions internal/deploy/state_v1_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func (lock *OperationLock) CommitStateV1(expected *EnvironmentGenerationState, s
if !reflect.DeepEqual(current.IdentityMapping, state.IdentityMapping) {
return fmt.Errorf("identity authority changed or was omitted before state commit")
}
if current.IdentityMapping != nil && current.IdentityMapping.PendingOperation != nil && !reflect.DeepEqual(observed, state.Current) {
return fmt.Errorf("pending identity mapping operation %s must finish before changing the current generation", current.IdentityMapping.PendingOperation.OperationID)
}
if err := validateMappingOwnerPathV1(path, state); err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions internal/dockerdeploy/build_publication.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ func publishBuild(
return deploy.StateV1{}, fmt.Errorf("publication cannot replace another environment's owner context")
}
}
if state.IdentityMapping != nil && state.IdentityMapping.PendingOperation != nil {
return deploy.StateV1{}, fmt.Errorf("pending identity mapping operation %s must finish before build publication", state.IdentityMapping.PendingOperation.OperationID)
}
var old *deploy.EnvironmentGenerationState
var oldImage *providers.RealizedImageV1
var oldPairs []OwnedImageReferenceV1
Expand Down
49 changes: 41 additions & 8 deletions internal/dockerdeploy/identity_mapping_input.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,66 @@
package dockerdeploy

import (
"context"
"fmt"
"path/filepath"

"github.com/omry/reploy/internal/blueprint"
"github.com/omry/reploy/internal/deploy"
"github.com/omry/reploy/internal/identitymapping"
)

// This private metadata seam does not allocate IDs or admit a workload.
// B5 will bind its candidate to the selected generation and mapping revision.
func loadStagedIdentityMappingV1(operation *deploy.OperationLock, dir string) (*identitymapping.Geometry, error) {
_, candidate, err := loadSelectedIdentityMappingV1(operation, dir, "")
return candidate, err
}

func loadSelectedIdentityMappingV1(operation *deploy.OperationLock, dir, overrideFile string) (deploy.StateV1, *identitymapping.Geometry, error) {
if operation == nil {
return nil, fmt.Errorf("mapping input requires a deployment operation lock")
return deploy.StateV1{}, nil, fmt.Errorf("mapping input requires a deployment operation lock")
}
state, found, err := operation.ReadStateV1()
if err != nil {
return nil, err
return state, nil, err
}
if !found || state.Deployment != nil || state.Staging == nil {
return nil, fmt.Errorf("mapping input requires selected staging state")
return state, nil, fmt.Errorf("mapping input requires selected staging state")
}
absolute, err := filepath.Abs(dir)
if err != nil || operation.Path() != filepath.Join(absolute, ".reploy", "operation.lock") {
return state, nil, fmt.Errorf("mapping input lock does not belong to the selected deployment")
}
document, err := blueprint.DecodeResolvedDocumentV1(state.Blueprint)
if err != nil {
return nil, err
return state, nil, err
}
var overrides deploy.PackageOverridesV1
if overrideFile == "" {
overrides, _, _, err = LoadStagedPackageOverridesV1(operation, dir, document)
} else {
overrides, found, err = deploy.ReadPackageOverridesFileV1(overrideFile)
Comment thread
omry marked this conversation as resolved.
if err == nil && !found {
err = fmt.Errorf("selected mapping override file does not exist")
}
if err == nil && overrides.Environment.ID != document.Environment.ID {
err = fmt.Errorf("selected mapping override targets a different environment")
}
if err == nil {
err = overrides.CheckIdentityMappingDeclaration(document)
}
}
overrides, _, _, err := LoadStagedPackageOverridesV1(operation, dir, document)
if err != nil {
return nil, err
return state, nil, err
}
candidate, err := deploy.ResolveIdentityMappingGeometryV1(document, overrides.IdentityMappingOverride())
return state, candidate, err
}

func configureStagedIdentityMappingV1(ctx context.Context, operation *deploy.OperationLock, dir, overrideFile, operationID string, evidence deploy.IdentityMappingEvidenceV1) (deploy.StateV1, error) {
state, candidate, err := loadSelectedIdentityMappingV1(operation, dir, overrideFile)
if err != nil || candidate == nil {
return state, fmt.Errorf("mapping configuration requires a complete declared mapping: %v", err)
}
return deploy.ResolveIdentityMappingGeometryV1(document, overrides.IdentityMappingOverride())
return (deploy.IdentityMappingOperationsV1{Operation: operation, Evidence: evidence}).Configure(ctx, state, *candidate, operationID)
}
Loading
Loading