Skip to content

Commit 9f362af

Browse files
committed
Fix fork name collisions and running-source restore ordering
1 parent 834f2d4 commit 9f362af

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

lib/instances/fork.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"hash/crc32"
99
"os"
10+
"path/filepath"
1011
"strings"
1112
"time"
1213

@@ -21,7 +22,7 @@ import (
2122

2223
// forkInstance creates a new instance by cloning a stopped or standby source
2324
// instance. It returns the newly created fork and the requested final target
24-
// state; callers apply the target state transition outside the source lock.
25+
// state; callers apply remaining target state transitions outside the source lock.
2526
func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceRequest) (*Instance, State, error) {
2627
log := logger.FromContext(ctx)
2728
log.InfoContext(ctx, "forking instance", "source_instance_id", id, "fork_name", req.Name)
@@ -68,6 +69,22 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR
6869
}
6970
}
7071
}
72+
73+
// For Firecracker running-source forks, restoring the fork may temporarily alias
74+
// the source data directory. Restore the fork while source remains standby and
75+
// under lock, then restore the source.
76+
if forkErr == nil && targetState == StateRunning {
77+
restoredFork, err := m.applyForkTargetState(ctx, forked.Id, StateRunning)
78+
if err != nil {
79+
forkErr = fmt.Errorf("restore forked instance before source restore: %w", err)
80+
if cleanupErr := m.cleanupForkInstanceOnError(ctx, forked.Id); cleanupErr != nil {
81+
forkErr = fmt.Errorf("%v; additionally failed to cleanup forked instance %s: %v", forkErr, forked.Id, cleanupErr)
82+
}
83+
} else {
84+
forked = restoredFork
85+
}
86+
}
87+
7188
log.InfoContext(ctx, "restoring source instance after running fork", "source_instance_id", id)
7289
_, restoreErr := m.restoreInstance(ctx, id)
7390

@@ -193,6 +210,13 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
193210
return nil, err
194211
}
195212

213+
existsByMetadata, err := m.instanceNameExists(req.Name)
214+
if err != nil {
215+
return nil, fmt.Errorf("check instance name availability: %w", err)
216+
}
217+
if existsByMetadata {
218+
return nil, fmt.Errorf("%w: instance name '%s' already exists", ErrAlreadyExists, req.Name)
219+
}
196220
if stored.NetworkEnabled {
197221
exists, err := m.networkManager.NameExists(ctx, req.Name, "")
198222
if err != nil {
@@ -325,6 +349,25 @@ func validateForkVolumeSafety(volumes []VolumeAttachment) error {
325349
return nil
326350
}
327351

352+
func (m *manager) instanceNameExists(name string) (bool, error) {
353+
metaFiles, err := m.listMetadataFiles()
354+
if err != nil {
355+
return false, err
356+
}
357+
358+
for _, metaFile := range metaFiles {
359+
id := filepath.Base(filepath.Dir(metaFile))
360+
meta, err := m.loadMetadata(id)
361+
if err != nil {
362+
continue
363+
}
364+
if meta.Name == name {
365+
return true, nil
366+
}
367+
}
368+
return false, nil
369+
}
370+
328371
func resolveForkTargetState(requested State, sourceState State) (State, error) {
329372
if requested == "" {
330373
switch sourceState {

lib/instances/fork_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,52 @@ func TestForkInstance_CleansUpOnTargetTransitionError(t *testing.T) {
159159
assert.Equal(t, sourceID, entries[0].Name())
160160
}
161161

162+
func TestForkInstanceRejectsDuplicateNameForNonNetworkedSource(t *testing.T) {
163+
manager, _ := setupTestManager(t)
164+
ctx := context.Background()
165+
166+
sourceID := "fork-duplicate-name-source"
167+
require.NoError(t, manager.ensureDirectories(sourceID))
168+
169+
now := time.Now()
170+
sourceMeta := &metadata{StoredMetadata: StoredMetadata{
171+
Id: sourceID,
172+
Name: sourceID,
173+
Image: "docker.io/library/alpine:latest",
174+
CreatedAt: now,
175+
StoppedAt: &now,
176+
HypervisorType: hypervisor.TypeCloudHypervisor,
177+
HypervisorVersion: "test",
178+
SocketPath: paths.New(manager.paths.DataDir()).InstanceSocket(sourceID, "cloud-hypervisor.sock"),
179+
DataDir: paths.New(manager.paths.DataDir()).InstanceDir(sourceID),
180+
VsockCID: 42,
181+
VsockSocket: paths.New(manager.paths.DataDir()).InstanceVsockSocket(sourceID),
182+
}}
183+
require.NoError(t, manager.saveMetadata(sourceMeta))
184+
185+
existingID := "fork-duplicate-name-existing"
186+
require.NoError(t, manager.ensureDirectories(existingID))
187+
existingMeta := &metadata{StoredMetadata: StoredMetadata{
188+
Id: existingID,
189+
Name: "duplicate-name",
190+
Image: "docker.io/library/alpine:latest",
191+
CreatedAt: now,
192+
StoppedAt: &now,
193+
HypervisorType: hypervisor.TypeCloudHypervisor,
194+
HypervisorVersion: "test",
195+
SocketPath: paths.New(manager.paths.DataDir()).InstanceSocket(existingID, "cloud-hypervisor.sock"),
196+
DataDir: paths.New(manager.paths.DataDir()).InstanceDir(existingID),
197+
VsockCID: 43,
198+
VsockSocket: paths.New(manager.paths.DataDir()).InstanceVsockSocket(existingID),
199+
}}
200+
require.NoError(t, manager.saveMetadata(existingMeta))
201+
202+
_, err := manager.ForkInstance(ctx, sourceID, ForkInstanceRequest{Name: "duplicate-name"})
203+
require.Error(t, err)
204+
assert.ErrorIs(t, err, ErrAlreadyExists)
205+
assert.Contains(t, err.Error(), "already exists")
206+
}
207+
162208
func TestCloneStoredMetadataForFork_DeepCopiesReferenceFields(t *testing.T) {
163209
startedAt := time.Now().Add(-2 * time.Minute)
164210
stoppedAt := time.Now().Add(-1 * time.Minute)

0 commit comments

Comments
 (0)