Skip to content

feat(cmd): Add '--rollout' flag when creating - #309

Draft
craciunoiuc wants to merge 2 commits into
prod-stagingfrom
craciunoiuc/add-rollout
Draft

feat(cmd): Add '--rollout' flag when creating#309
craciunoiuc wants to merge 2 commits into
prod-stagingfrom
craciunoiuc/add-rollout

Conversation

@craciunoiuc

@craciunoiuc craciunoiuc commented May 7, 2026

Copy link
Copy Markdown
Contributor

Open to UI/UX suggestions, right now it prints every new instance that was rolled

Closes: TOOL-794

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds support for a --rollout mode to instance create, intended to replace existing instances in the same service group that use the same image “base name”, while also adjusting how service-group and volume identifiers are parsed during instance creation.

Changes:

  • Add --rollout flag to instance create and route execution through a new runRollout workflow.
  • Modify parsing behavior for --service and --volume-style inputs (notably InstanceService.UnmarshalText and InstanceVolume.UnmarshalText).
  • Remove the client-side “volume metro mismatch” validation during instance creation.
Comments suppressed due to low confidence (1)

internal/cmd/instances.go:783

  • The volume creation path no longer validates vol.Metro against the instance metro (unlike the service-group check below). If a volume is specified via structured input (--set volumes.0.metro=..., JSON/YAML, or if text parsing supports metro/...), the CLI will now proceed and fail later with a less actionable API error; consider restoring the metro-mismatch validation for volumes when vol.Metro is set.
		case "volumes":
			for _, vol := range field.Create.Set.([]*InstanceVolume) {
				reqVol := platform.CreateInstanceRequestVolume{
					At: vol.At,
				}
				if vol.UUID != "" {
					reqVol.Uuid = &vol.UUID
				}
				if vol.Name != "" {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from 926d9db to ce661f7 Compare May 7, 2026 15:15
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch 2 times, most recently from 8ccf7d3 to c3adcf6 Compare May 12, 2026 15:44
@craciunoiuc

Copy link
Copy Markdown
Contributor Author

I reworked the implementation as you asked @jedevc

Right now only instance rollout is implemented, but we can do other also I think

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

internal/cmd/instances.go:904

  • The volume link type (Link[Volume]) includes a Metro field, but the create path no longer validates that an explicitly metro-qualified volume matches the instance metro. Without this check, a user can specify e.g. "sfo/myvol" while creating in another metro and the metro will be silently ignored in the API request (only Name/UUID are sent). Consider restoring the metro-mismatch validation (similar to the service group check) or explicitly rejecting metro-qualified volume references here.
		case "volumes":
			for _, vol := range field.Create.Set.([]*InstanceVolume) {
				reqVol := platform.CreateInstanceRequestVolume{
					At: vol.At,
				}
				if vol.UUID != "" {
					reqVol.Uuid = &vol.UUID
				}
				if vol.Name != "" {
					reqVol.Name = &vol.Name
				}
				if vol.Size > 0 {
					reqVol.SizeMb = new(uint64(vol.Size))
				}
				if vol.Readonly {
					reqVol.Readonly = &vol.Readonly
				}
				req.Volumes = append(req.Volumes, reqVol)
			}

Comment thread internal/resource/cmd/cmd.go
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch 2 times, most recently from 143ed09 to 89cd18a Compare May 13, 2026 10:05
@craciunoiuc
craciunoiuc requested a review from Copilot May 15, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

internal/cmd/instances.go:1229

  • Calling Instance{}.Delete directly bypasses the sandbox wrappers used by resource commands, so --rollout can delete matching service instances even when they are not tracked by the active sandbox. This breaks sandbox isolation and can remove resources that normal delete commands would reject.
		delErr := (Instance{}).Delete(ctx, []resource.Resource{old})

Comment thread internal/cmd/instances.go
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/resource/cmd/cmd.go Outdated
Comment thread internal/resource/cmd/cmd.go
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch 5 times, most recently from c68b276 to 144843c Compare May 18, 2026 19:02
@craciunoiuc

Copy link
Copy Markdown
Contributor Author

something funky going on with tests, probably server is misbehaving. Will need to wait till I can make sure tests pass

Comment thread internal/cmd/instances.go Outdated
Comment thread internal/cmd/instances.go Outdated
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from 144843c to a2e506a Compare June 2, 2026 12:06
@craciunoiuc

Copy link
Copy Markdown
Contributor Author

One thing left to discuss here is if we're going for the loop-wait-retry logic

If yes, then feel free to merge, if not, then we got to wait for the platform changes to go in

I would say we leave as is because a rollout can take significantly more than a simple start or restart, si it's fine to have a more complex logic in place (at least conceptually)

@craciunoiuc
craciunoiuc requested a review from jedevc June 2, 2026 12:20
This adds sequential rolling over services

Signed-off-by: Cezar Craciunoiu <cezar@unikraft.io>
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from a2e506a to d0260b3 Compare July 28, 2026 08:01
Signed-off-by: Cezar Craciunoiu <cezar@unikraft.io>
@jedevc

jedevc commented Jul 28, 2026

Copy link
Copy Markdown
Member

Before merging the timeouts stuff here, could we get #223 in first?

Comment thread internal/cmd/instances.go
Comment on lines +1209 to +1211
if rollout && req.Image == nil && req.Template == nil {
return nil, fmt.Errorf("--rollout requires an image or template (use --image or --template)")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Unnecessary, duplicate check below.

Comment thread internal/cmd/instances.go
return results, nil
}

func (Instance) Rollout(ctx context.Context, created []resource.Resource, fields []resource.Field) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Which of this logic is actually specific to just instances. I was imagining that we would just be able to use the existing delete methods.

If it's this specialized, there's no point in it being a special case like this, your choice to go back, or to make it more generic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At some point we wanted to do rollout for all resources, not just instances

But each wait and creation is unique so you got to implement this in every resource right?

Comment thread internal/cmd/instances.go
if inst.Image.Reference == nil {
continue
}
if inst.Image.Reference.Name() == newImageName {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought we didn't want to do rollouts based on image name - we moved over from doing that in kraftlet.

I think we should just do it based on the service.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I didn't know about this?

So just rollout everything in a service?

Comment thread internal/cmd/instances.go
Comment on lines +1380 to +1382
// Create an additional replacement instance with a modified name.
rolloutFields := slices.Clone(fields)
for j := range rolloutFields {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are we doing more creation after our existing creation? We should do all the creation in one step, then do all our deletions.

Also, surely we could do bulk creation using replicas.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well this is the exact opposite of what we want 😓

If you have a service of max size 16 (default), and you have 9 instances in it, it means you can't rollout

If you do it sequentially, it means it will actually fit even if you are at 15/16 used

Alternatively we can add a --sequential flag to rollout

Comment thread internal/cmd/instances.go
Comment on lines +1400 to +1407
if waitErr := group.DoMetro(ctx, g, metro, func(ctx context.Context, mc multimetro.MetroClient) error {
_, err := mc.WaitInstanceByUUID(ctx, newInst.UUID, platform.WaitInstanceByUUIDRequestBody{
State: platform.InstanceStateRunning,
})
return err
}); waitErr != nil {
return fmt.Errorf("waiting for new instance to be running: %w", waitErr)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this should be configurable - we should be able to configure some timeout, in which we check that after the instance has started running that it is still running after t time.

We don't really have a good way to check the health of an instance, so we need some way to make sure that we don't start up an instance, it immediately crashes, and then we delete the old one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How would that flag look like like --recheck 5s

Aka, wait 5s and check if the instance is still running? I can add that, but one could argue that it's on them if they deploy a service that crashes after X seconds

If it were to crash at boot it would do starting -> stopped without going into running I think

Comment thread internal/cmd/instances.go
if extra := len(toRemove) - len(created); extra > 0 {
var baseName string
for key, field := range resource.IterFields(fields) {
if key.String() == "name" && field.Create != nil && field.Create.Set != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

resource.GetFieldByPath instead of iteration.

Comment thread internal/cmd/instances.go
Features []string `group:"flag-create" shortcut:"features" help:"Instance features." placeholder:"feature"`
Template string `group:"flag-create" shortcut:"template" help:"Create from instance template." placeholder:"name"`
DeleteOnStop bool `group:"flag-create" name:"rm" help:"Automatically delete the instance when it stops."`
Rollout bool `group:"flag-create" shortcut:"rollout" help:"Replace old instances with new ones matching image name"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we should rename this to be more specific --service-rollout. We rollout on a service.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The original intent/discussion was to move this to be generic and implementable for all resources

Not sure if we still want that?

Comment thread internal/cmd/instances.go
Comment on lines +1206 to +1208
if rollout && req.Replicas != nil && *req.Replicas > 0 {
return nil, fmt.Errorf("--replicas cannot be used with --rollout")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It would make the math complicated and confusing? What would replicas mean in this context?

If I'm rolling ~6 instances in a service, and I specify 2 replicas, does it mean I want 2 instances in the end or 6*2==12?

We would need to be very clear in this case

Delete(ctx context.Context, keys []string) error
}

type RolloutableResource interface {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In hindsight, maybe this abstraction isn't right. Apologies.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

😭

@jedevc

jedevc commented Jul 28, 2026

Copy link
Copy Markdown
Member

A couple thoughts. There are two modes of rollout I think:

  • "Rolling" rollout. As implemented, create new instances, wait for healthy, delete old ones (or delete new ones if never healthy). Very nice, everyone is happy.
  • "Replace" rollout. Not implemented, but creates new instances, stops old instances, starts new ones, wait for healthy, delete old ones (or delete new ones, and restore old ones).
    • Imagine a database. Each running database needs exclusive access to it's volume, so we should only actually be running one at a time. We'd still like to be able to do rollout, but we can't start any in parallel, so we should try and stop the old one and restart it later if the rollout fails.

Does that make sense? I don't really know what to call these "modes", but I think we should have both of them.

Also, as far as I can tell, the current implementation doesn't have the failure mode, where we restore back to the "known" good state. It assumes that every rollout is perfectly successful, which it may not be.

@craciunoiuc

Copy link
Copy Markdown
Contributor Author

for rolling -- true, there is no rolling back, but you can't really do that if you're deleting instances while you're going 😅

for replace -- yes, we could do that, and I guess that should also allow for doing the health check after some time, and would only use the same amount of space in the service group.

I think we can do both, with rolling being the fast "destructive" one that just waits for instances to start, and replace being he "safe" one

With the small note that running a database in a service group is conceptually wrong so if we want to truly support database rollout we need to support non-servicegroup based rollout

@jedevc

jedevc commented Jul 28, 2026

Copy link
Copy Markdown
Member

With the small note that running a database in a service group is conceptually wrong so if we want to truly support database rollout we need to support non-servicegroup based rollout

Hm, so how would you imagine database rollout working? I think service group still makes sense here - it's about the database service that you care about, but I'm curious why this is wrong.

for rolling -- true, there is no rolling back, but you can't really do that if you're deleting instances while you're going 😅

Indeed, that's why I was suggesting creating them all, and only deleting once you're convinced of the health of the others. Similar to how rolling deployments work in k8s.

I think we can do both, with rolling being the fast "destructive" one that just waits for instances to start, and replace being he "safe" one

I think the safety of them is the same no? replace results in downtime, but ensures that we never have more than n instances actively running, while rolling potentially allows up to 2n instances running, but never dipping below n.

I don't actually know actually if the names rolling/replace are right here. Maybe there's some better name here, or some better way of expressing the constraint (like max_running = n).

jedevc commented Jul 28, 2026

Copy link
Copy Markdown
Member

Note, the platform doesn't really have a concept of instance health… this is sad.

IMO, we should currently think of healthy as:

  • Instance starts and enters a running state
  • We monitor the instance for "time"
  • We check that the instance is still running
  • Note instance as healthy

However, as we build the rollout flag, etc, we should consider that at some point, we would add a "real" healthcheck system (or add the ability to monitor health in new ways, like by checking logs, etc)

@craciunoiuc
craciunoiuc marked this pull request as draft July 28, 2026 12:15
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.

3 participants