Skip to content

feat!: Add instance branching + checkpointing - #403

Open
jedevc wants to merge 31 commits into
prod-stagingfrom
jedevc/instance-branching
Open

feat!: Add instance branching + checkpointing#403
jedevc wants to merge 31 commits into
prod-stagingfrom
jedevc/instance-branching

Conversation

@jedevc

@jedevc jedevc commented Jul 7, 2026

Copy link
Copy Markdown
Member

TODO:

  • Quick design once-over

Signed-off-by: Justin Chadwell <justin@unikraft.com>
Comment thread cmd/unikraft/integration/instance_test.go Outdated
jedevc and others added 3 commits July 31, 2026 14:48
Signed-off-by: Justin Chadwell <justin@unikraft.com>
Signed-off-by: ayn <ayn.khatib@gmail.com>
Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar
khatibomar force-pushed the jedevc/instance-branching branch 2 times, most recently from 384f0e7 to f78fa9b Compare July 31, 2026 17:40
Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar
khatibomar force-pushed the jedevc/instance-branching branch from f78fa9b to a2224e5 Compare July 31, 2026 17:46
--branch and --checkpoint parsed a metro/name ref but silently dropped
the metro part before sending the request to a single metro client,
unlike the sibling template/volumes/service handling in the same
function. A cross-metro ref could silently target a same-named but
wrong resource, or fail with a misleading error.

Signed-off-by: ayn <ayn.khatib@gmail.com>
InstanceCheckpoint.Get() matched API response items to requested refs
by array position, unlike every sibling Get() which uses matchRef. If
the platform reorders or omits entries, a checkpoint's tracked
identity (Key()) could get swapped with another's.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Create() discarded successfully-created checkpoint keys whenever any
sibling instance in the same batch failed, because the outer loop
skipped the append on error. This left orphaned, untracked checkpoints
on the platform while the CLI reported total failure.

Signed-off-by: ayn <ayn.khatib@gmail.com>
getInstanceHistory() skipped only entries with an explicit non-success
status, so a nil Status fell through and was treated as successful.
Every other status check in this file (and codebase) treats a missing
status as failure, not success.

Signed-off-by: ayn <ayn.khatib@gmail.com>
--image, --template, --branch, and --checkpoint are mutually exclusive
per the SDK docs, but only the roms field enforced a similar pattern.
Reject the request when more than one source is set instead of
silently letting one win.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Signed-off-by: ayn <ayn.khatib@gmail.com>

Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar
khatibomar force-pushed the jedevc/instance-branching branch from 3ec0c39 to 08d8f90 Compare July 31, 2026 19:40
Signed-off-by: ayn <ayn.khatib@gmail.com>
InstanceCheckpoint.Create() never set the SDK's TimeoutS field, so
there was no way to wait for a checkpoint to reach its terminal state
before restoring from it. Wire a wait-timeout field through, matching
the existing pattern on instance create, and use it in the
create-from-checkpoint and checkpoint-state integration tests to
close a race against checkpoint readiness.

Signed-off-by: ayn <ayn.khatib@gmail.com>
HTTPPost retried the non-idempotent POST /increment call on any
transport or read error, including cases where the server had already
applied the increment before the response was lost. Send a stable
Idempotency-Key header across retries and have the test counter
server dedupe by key so repeated deliveries return the cached result
instead of re-applying the delta.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Every existing checkpoint create case passed a single instance target,
so the multi-ref creation loop and Get() ref-matching logic never ran
against 2+ items.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Signed-off-by: ayn <ayn.khatib@gmail.com>
Signed-off-by: ayn <ayn.khatib@gmail.com>
The identical delete-lock field on instances already has a dedicated
test; checkpoints had none.

Signed-off-by: ayn <ayn.khatib@gmail.com>
create-from-checkpoint only checked the restored instance's name,
not that image/memory/vcpus were inherited from the checkpoint.

Signed-off-by: ayn <ayn.khatib@gmail.com>
The SDK documents --branch as supporting running, stopped, or template
sources, but only the running case was tested.

Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar
khatibomar marked this pull request as ready for review August 4, 2026 16:06
restart-follow needs more than 5s to catch the second boot's log line
since restart has extra stop/start overhead over a plain start.

branch-stopped and branch-template are skipped: verified live against
staging that the backend does not preserve in-guest memory state across
a stop, and branch_from cannot resolve template-state sources at all.

Signed-off-by: ayn <ayn.khatib@gmail.com>
// instance is stopped rather than running, and that the branched
// instance carries over the state the source had when it was stopped.
t.Run("branch-stopped", func(t *testing.T) {
// NOTE: Remove once branching from a stopped source carries over its

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is weird, is that the expected behavior currently?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is weird. We should check that branching from a stopped state works, but the application isn't running.

Instead, we would need to check something from disk, not from memory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    	Error Trace:	/home/runner/_work/cli/cli/cmd/unikraft/integration/instance_test.go:1264
    	Error:      	"{\"count\": 0}" does not contain "\"count\": 5"
    	Test:       	TestInstances/branch-stopped

It seems it doesn't copy the fs either.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a skip also for the disk test for now

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, worth checking actually. This works for not branched instances right? Where a start/stop, keeps the disk?

If it does, and branching isn't copying the disk contents, then something feels off to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on tests that I tried the root disk get wiped on reboot, regardless if it's branched or not. Which means reboot/stop instance itself wipes root disk. Only attached volumes persist.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay, welp. Branching should copy the volumes - so we can test that then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, added

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I missing it? I don't see it pushed.

(also needs rebasing).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

91ce09f

this commit

Comment thread internal/cmd/instances.go Outdated
Comment thread cmd/unikraft/integration/instance_checkpoint_test.go Outdated
Comment thread cmd/unikraft/testdata/TestHelp/instances Outdated
Comment thread internal/cmd/instance_checkpoints.go Outdated
Comment on lines +338 to +339
case "instances":
instances = field.Create.Set.([]string)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this is weird, but it is like templates. I wonder if it's worth making this a breaking change, and making both checkpoint and template only accept a single instance. It feels weird to accept multiple.

Returning multiple resources from create was only really intended for handling --replicas.

WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have some usage metrics ( which I believe can be disabled by users )
So we can kinda predict if this breaking change is a big deal or not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot_20260806_162825

I think we safe to do that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread internal/cmd/instance_checkpoints.go Outdated
Comment on lines +379 to +387
// Create checkpoints one at a time since the platform API only accepts single operations
for _, ref := range refs {
refStr := cmp.Or(ref.Name, ref.UUID)
log.G(ctx).Trace().Str("ref", refStr).Msg("creating instance checkpoint")
req := platform.CreateCheckpointInstancesRequestItem{
From: ref.NameOrUUID(),
TimeoutS: timeoutS,
}
resp, err := c.CreateCheckpointInstances(ctx, []platform.CreateCheckpointInstancesRequestItem{req})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we do decide to support bulk requests, you should be able to attach multiple reqs. Similar for templates.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • instance_checkpoints.go:
    var errs []error
    // Create checkpoints one at a time since the platform API only accepts single operations
    for _, ref := range refs {
    refStr := cmp.Or(ref.Name, ref.UUID)
    log.G(ctx).Trace().Str("ref", refStr).Msg("creating instance checkpoint")
    req := platform.CreateCheckpointInstancesRequestItem{
    From: ref.NameOrUUID(),
    TimeoutS: timeoutS,
    }
  • instance_templates.go:
    // Create templates one at a time since the platform API only accepts single operations
    for _, ref := range refs {
    refStr := cmp.Or(ref.Name, ref.UUID)
    var reqItem platform.CreateTemplateInstancesRequestItem
    if ref.Name != "" {
    reqItem.Name = new(ref.Name)
    } else {
    reqItem.Uuid = new(ref.UUID)
    }
    log.G(ctx).Trace().Str("ref", refStr).Msg("creating instance template")
    resp, err := c.CreateTemplateInstances(
    ctx,
    []platform.CreateTemplateInstancesRequestItem{
    reqItem,
    },
    )
  • volume_templates.go:
    // Create templates one at a time since the platform API only accepts single operations
    for _, ref := range refs {
    refStr := cmp.Or(ref.Name, ref.UUID)
    log.G(ctx).Trace().Str("ref", refStr).Msg("creating volume template")
    resp, err := c.CreateTemplateVolume(ctx, []platform.NameOrUUID{ref.NameOrUUID()})

Ham templates following the same pattern

Comment thread internal/cmd/testdata/TestOutput/instance-history Outdated
The checkpoint subcommand's help group key was never registered with
kong.ExplicitGroups, so it rendered as the raw key "cmd-checkpoints"
instead of "Checkpoints:" like the other resource groups.

Signed-off-by: ayn <ayn.khatib@gmail.com>
The table printer skipped a field whenever its Go zero value was
reflect-zero, even when the field renders meaningful text at that
value (e.g. a zero RelativeTime renders as "never"). This left the
CREATED column blank under a populated header instead of showing
"never", inconsistent with the kv-all view.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Replaces the manual Name/UUID field-by-field construction of
platform.NameOrUUID with the existing group.Ref.NameOrUUID() helper.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Checkpoints could only be polled via repeated inspect calls, so the
checkpoint test tolerated a transient "starting" state instead of
waiting for it to settle. Wire up the generic WaitableResourceCmd and
have the test wait for state==checkpoint instead.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Branching from a stopped source can't carry over in-guest memory
(stopping doesn't take a memory snapshot), so the test was skipped
entirely rather than verifying anything meaningful. Persist the
counter to disk in the test app and read it back on boot, so the test
can verify what branching from a stopped instance actually should
carry over: the source's disk state.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Verified live against staging: branching from a stopped instance
always boots the branch with a fresh, empty disk (count resets to 0),
so the assertion added in a9d3708 fails consistently rather than
flaking. Same backend gap already documented for branch-template.

Signed-off-by: ayn <ayn.khatib@gmail.com>

Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar khatibomar changed the title feat: Add instance branching + checkpointing feat!: Add instance branching + checkpointing Aug 6, 2026
Accepting multiple instances per invocation was inconsistent with the
platform API, which only ever creates one checkpoint/template at a
time anyway. Narrow both commands to a single instance argument.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Signed-off-by: ayn <ayn.khatib@gmail.com>
Branching copies attached volumes, so use one to verify state survives a
branch of a stopped instance, instead of the root disk which never
survives a stop. Verified against staging that the branch gets an
independent copy of the volume.

Signed-off-by: ayn <ayn.khatib@gmail.com>
@jedevc

jedevc commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@khatibomar can you resolve the code comments that are resolved?

@khatibomar

khatibomar commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

I did close the ones that I don't think they need follow up, the opened ones, need some input or just an ACK

Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar
khatibomar force-pushed the jedevc/instance-branching branch from bde63fb to fda2109 Compare August 7, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants