@@ -66,7 +66,9 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR
6666 return nil , "" , fmt .Errorf ("standby source instance: %w" , err )
6767 }
6868
69- forked , forkErr := m .forkInstanceFromStoppedOrStandby (ctx , id , req , true )
69+ // Running fork is a one-shot clone that restores the source afterward;
70+ // promoting to Template would block the restore (templates can't wake).
71+ forked , forkErr := m .forkInstanceFromStoppedOrStandby (ctx , id , req , true , true )
7072 if forkErr == nil {
7173 if err := m .rotateSourceVsockForRestore (ctx , id , forked .Id ); err != nil {
7274 forkErr = fmt .Errorf ("prepare source snapshot for restore: %w" , err )
@@ -104,14 +106,14 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR
104106 return nil , "" , forkErr
105107 }
106108 return forked , targetState , nil
107- case StateStopped , StateStandby :
108- forked , err := m .forkInstanceFromStoppedOrStandby (ctx , id , req , false )
109+ case StateStopped , StateStandby , StateTemplate :
110+ forked , err := m .forkInstanceFromStoppedOrStandby (ctx , id , req , false , false )
109111 if err != nil {
110112 return nil , "" , err
111113 }
112114 return forked , targetState , nil
113115 default :
114- return nil , "" , fmt .Errorf ("%w: cannot fork from state %s (must be Stopped or Standby , or Running with from_running=true)" , ErrInvalidState , source .State )
116+ return nil , "" , fmt .Errorf ("%w: cannot fork from state %s (must be Stopped, Standby, or Template , or Running with from_running=true)" , ErrInvalidState , source .State )
115117 }
116118}
117119
@@ -193,7 +195,7 @@ func generateForkSourceVsockCID(sourceID, forkID string, current int64) int64 {
193195 return cid
194196}
195197
196- func (m * manager ) forkInstanceFromStoppedOrStandby (ctx context.Context , id string , req ForkInstanceRequest , supportValidated bool ) (* Instance , error ) {
198+ func (m * manager ) forkInstanceFromStoppedOrStandby (ctx context.Context , id string , req ForkInstanceRequest , supportValidated , skipTemplatePromotion bool ) (* Instance , error ) {
197199 log := logger .FromContext (ctx )
198200
199201 meta , err := m .loadMetadata (id )
@@ -205,10 +207,10 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
205207 stored := & meta .StoredMetadata
206208
207209 switch source .State {
208- case StateStopped , StateStandby :
210+ case StateStopped , StateStandby , StateTemplate :
209211 // allowed
210212 default :
211- return nil , fmt .Errorf ("%w: cannot fork from state %s (must be Stopped or Standby )" , ErrInvalidState , source .State )
213+ return nil , fmt .Errorf ("%w: cannot fork from state %s (must be Stopped, Standby, or Template )" , ErrInvalidState , source .State )
212214 }
213215
214216 if ! supportValidated {
@@ -250,7 +252,9 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
250252 })
251253 defer cu .Clean ()
252254
253- if source .State == StateStandby {
255+ fromSnapshot := source .State == StateStandby || source .State == StateTemplate
256+
257+ if fromSnapshot {
254258 if err := m .ensureSnapshotMemoryReady (ctx , m .paths .InstanceSnapshotLatest (id ), m .snapshotJobKeyForInstance (id ), stored .HypervisorType ); err != nil {
255259 return nil , fmt .Errorf ("prepare standby snapshot for fork: %w" , err )
256260 }
@@ -286,17 +290,23 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
286290 // phase (Standby for snapshot forks, Stopped for stopped forks) will be
287291 // recorded by the appropriate operation when the fork is acted on.
288292 forkMeta .Phases .Reset ()
289- switch source .State {
290- case StateStandby :
293+ if fromSnapshot {
291294 forkMeta .Phases .Record (phasetracking .PhaseStandby , now )
292- case StateStopped :
295+ } else {
293296 forkMeta .Phases .Record (phasetracking .PhaseStopped , now )
294297 }
295298
299+ // Template-only fields don't carry forward to the fork; the fork is a fresh
300+ // instance regardless of whether the parent is a template.
301+ forkMeta .IsTemplate = false
302+ forkMeta .ForkCount = 0
303+ forkMeta .HotPagesPath = ""
304+ forkMeta .ForkOfTemplate = ""
305+
296306 // Keep the original CID for snapshot-based forks.
297307 // Rewriting CID in restored memory snapshots is not reliable across
298308 // hypervisors.
299- if source . State == StateStandby {
309+ if fromSnapshot {
300310 forkMeta .VsockCID = stored .VsockCID
301311 } else {
302312 forkMeta .VsockCID = generateVsockCID (forkID )
@@ -309,7 +319,7 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
309319 forkMeta .MAC = ""
310320 }
311321
312- if source . State == StateStandby {
322+ if fromSnapshot {
313323 snapshotConfigPath := m .paths .InstanceSnapshotConfig (forkID )
314324 netCfg := (* hypervisor .ForkNetworkConfig )(nil )
315325 if forkMeta .NetworkEnabled {
@@ -331,6 +341,25 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
331341 }
332342 }
333343
344+ // Promote source to Template (or bump existing template ForkCount) so the
345+ // snapshot we just cloned can't be woken or deleted while this fork lives.
346+ // Skipped for the running-fork flow, where the source is restored afterward.
347+ if fromSnapshot && ! skipTemplatePromotion {
348+ priorIsTemplate := stored .IsTemplate
349+ priorForkCount := stored .ForkCount
350+ stored .IsTemplate = true
351+ stored .ForkCount = priorForkCount + 1
352+ if err := m .saveMetadata (meta ); err != nil {
353+ return nil , fmt .Errorf ("promote source to template: %w" , err )
354+ }
355+ cu .Add (func () {
356+ stored .IsTemplate = priorIsTemplate
357+ stored .ForkCount = priorForkCount
358+ _ = m .saveMetadata (meta )
359+ })
360+ forkMeta .ForkOfTemplate = stored .Id
361+ }
362+
334363 newMeta := & metadata {StoredMetadata : forkMeta }
335364 if err := m .saveMetadata (newMeta ); err != nil {
336365 return nil , fmt .Errorf ("save fork metadata: %w" , err )
@@ -384,6 +413,10 @@ func resolveForkTargetState(requested State, sourceState State) (State, error) {
384413 switch sourceState {
385414 case StateRunning , StateStandby , StateStopped :
386415 return sourceState , nil
416+ case StateTemplate :
417+ // Forks of a template are plain Standby instances; the fork itself
418+ // is never a template.
419+ return StateStandby , nil
387420 default :
388421 return "" , fmt .Errorf ("%w: cannot derive fork target state from source state %s" , ErrInvalidState , sourceState )
389422 }
0 commit comments