@@ -15,6 +15,7 @@ import (
1515 "github.com/kernel/hypeman/lib/hypervisor"
1616 "github.com/kernel/hypeman/lib/logger"
1717 "github.com/kernel/hypeman/lib/network"
18+ "github.com/kernel/hypeman/lib/templates"
1819 "github.com/nrednav/cuid2"
1920 "go.opentelemetry.io/otel/attribute"
2021 "gvisor.dev/gvisor/pkg/cleanup"
@@ -36,11 +37,22 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR
3637 return nil , "" , err
3738 }
3839
40+ resolvedID , tpl , err := m .resolveForkFromTemplateRequest (ctx , id , req )
41+ if err != nil {
42+ return nil , "" , err
43+ }
44+ id = resolvedID
45+
3946 meta , err := m .loadMetadata (id )
4047 if err != nil {
4148 return nil , "" , err
4249 }
4350 source := m .toInstance (ctx , meta )
51+ if tpl != nil {
52+ if err := validateForkResolvedFromTemplate (tpl , source .HypervisorType ); err != nil {
53+ return nil , "" , err
54+ }
55+ }
4456 targetState , err := resolveForkTargetState (req .TargetState , source .State )
4557 if err != nil {
4658 return nil , "" , err
@@ -65,7 +77,7 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR
6577 return nil , "" , fmt .Errorf ("standby source instance: %w" , err )
6678 }
6779
68- forked , forkErr := m .forkInstanceFromStoppedOrStandby (ctx , id , req , true )
80+ forked , forkErr := m .forkInstanceFromStoppedOrStandby (ctx , id , req , true , tpl )
6981 if forkErr == nil {
7082 if err := m .rotateSourceVsockForRestore (ctx , id , forked .Id ); err != nil {
7183 forkErr = fmt .Errorf ("prepare source snapshot for restore: %w" , err )
@@ -104,7 +116,7 @@ func (m *manager) forkInstance(ctx context.Context, id string, req ForkInstanceR
104116 }
105117 return forked , targetState , nil
106118 case StateStopped , StateStandby :
107- forked , err := m .forkInstanceFromStoppedOrStandby (ctx , id , req , false )
119+ forked , err := m .forkInstanceFromStoppedOrStandby (ctx , id , req , false , tpl )
108120 if err != nil {
109121 return nil , "" , err
110122 }
@@ -192,7 +204,7 @@ func generateForkSourceVsockCID(sourceID, forkID string, current int64) int64 {
192204 return cid
193205}
194206
195- func (m * manager ) forkInstanceFromStoppedOrStandby (ctx context.Context , id string , req ForkInstanceRequest , supportValidated bool ) (* Instance , error ) {
207+ func (m * manager ) forkInstanceFromStoppedOrStandby (ctx context.Context , id string , req ForkInstanceRequest , supportValidated bool , tpl * templates. Template ) (* Instance , error ) {
196208 log := logger .FromContext (ctx )
197209
198210 meta , err := m .loadMetadata (id )
@@ -202,6 +214,9 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
202214
203215 source := m .toInstance (ctx , meta )
204216 stored := & meta .StoredMetadata
217+ if tpl != nil && ! stored .IsTemplate {
218+ return nil , fmt .Errorf ("%w: template %s source instance %s is not flagged as a template parent" , ErrInvalidState , tpl .ID , id )
219+ }
205220
206221 switch source .State {
207222 case StateStopped , StateStandby :
@@ -255,12 +270,21 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
255270 }
256271 }
257272
258- if err := forkvm .CopyGuestDirectory (srcDir , dstDir ); err != nil {
273+ copyOpts := forkvm.CopyOptions {}
274+ if tpl != nil {
275+ copyOpts .SkipRelPaths = []string {templateSharedMemFileRelPath }
276+ }
277+ if err := forkvm .CopyGuestDirectoryWithOptions (srcDir , dstDir , copyOpts ); err != nil {
259278 if errors .Is (err , forkvm .ErrSparseCopyUnsupported ) {
260279 return nil , fmt .Errorf ("fork requires sparse-capable filesystem (SEEK_DATA/SEEK_HOLE unsupported): %w" , err )
261280 }
262281 return nil , fmt .Errorf ("clone guest directory: %w" , err )
263282 }
283+ if tpl != nil {
284+ if err := m .installForkSharedMemFile (dstDir , tpl ); err != nil {
285+ return nil , fmt .Errorf ("install shared mem-file: %w" , err )
286+ }
287+ }
264288
265289 starter , err := m .getVMStarter (stored .HypervisorType )
266290 if err != nil {
@@ -280,6 +304,15 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
280304 forkMeta .VsockSocket = m .paths .InstanceSocket (forkID , hypervisor .VsockSocketNameForType (forkMeta .HypervisorType ))
281305 forkMeta .ExitCode = nil
282306 forkMeta .ExitMessage = ""
307+ // Forks of a template carry the template id but never inherit the
308+ // IsTemplate flag — they are working copies.
309+ forkMeta .IsTemplate = false
310+ forkMeta .TemplateID = ""
311+ if tpl != nil {
312+ forkMeta .ForkOfTemplate = tpl .ID
313+ } else {
314+ forkMeta .ForkOfTemplate = stored .ForkOfTemplate
315+ }
283316
284317 // Keep the original CID for snapshot-based forks.
285318 // Rewriting CID in restored memory snapshots is not reliable across
@@ -324,6 +357,14 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
324357 return nil , fmt .Errorf ("save fork metadata: %w" , err )
325358 }
326359
360+ if tpl != nil {
361+ // Bumped before cu.Release so a refcount failure leaves no orphan
362+ // fork directory (deferred cu.Clean removes the data dir + metadata).
363+ if err := m .bumpTemplateForkRefcount (ctx , tpl ); err != nil {
364+ return nil , fmt .Errorf ("record template fork refcount: %w" , err )
365+ }
366+ }
367+
327368 cu .Release ()
328369 forked := m .toInstance (ctx , newMeta )
329370 log .InfoContext (ctx , "instance forked successfully" ,
0 commit comments