Skip to content

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

Open
craciunoiuc wants to merge 3 commits into
prod-stagingfrom
craciunoiuc/add-rollout
Open

craciunoiuc wants to merge 3 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
Completely reworked, now, as discussed below, added extra:

  • Two modes, rolling/replace, one starting + deleting, one stopping + starting + deleting
  • A configurable wait time to recheck that state is ok --rollout-healthy-after
  • Made only for instances in services

Notable ickies still open to suggestions:

  • Code needed to be added in the cmd resource to make sure that fields are accessed only once instead of twice
  • Not quite so plug-and-play: I split things in a rollout package, but still a lot of helpers were needed in the instance.go itself

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 Outdated
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 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 Outdated
@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
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from a2e506a to d0260b3 Compare July 28, 2026 08:01
@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 Outdated
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
Comment thread internal/cmd/instances.go Outdated
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 Outdated
Comment thread internal/cmd/instances.go Outdated
Comment thread internal/resource/resource.go Outdated
@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
@craciunoiuc

Copy link
Copy Markdown
Contributor Author

Ok so I think the summary is:

  • Two modes, rolling/replace doing what we said above
  • A rollout healthcheck flag to optionally recheck health after X seconds
  • Implemented just for instances with services for now, in the interest of time

jedevc commented Sep 7, 2026

Copy link
Copy Markdown
Member

Yup. For the last one, eventually we could do things like support rollout over tags. Might be worth planning what the CLI interface looks like? So it's nice and consistent for when we do.

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.

🟡 Changes recommended

There are confirmed correctness issues in the new rollout field override helper and in create-time save/validation behavior when wrappers override resolved create patches.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread internal/resource/cmd/cmd.go
Comment thread internal/resource/cmd/cmd.go Outdated
Comment thread internal/rollout/rollout.go Outdated
Comment thread internal/cmd/instances.go Outdated
@craciunoiuc craciunoiuc changed the title feat(cmd): Add '--rollout' flag when creating feat(cmd): Add rollout mechanics when creating Sep 7, 2026
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from 78e9056 to 6182238 Compare September 7, 2026 13:34
@craciunoiuc
craciunoiuc requested a lite review from Copilot September 9, 2026 12:03

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.

🟡 Changes recommended

The rollout “instances up” gate currently relies on a single immediate state read-back (and uses time.After in a cancelable wait), which can make rollouts flaky and incorrectly unwind.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread internal/cmd/instances.go
Comment thread internal/cmd/instances.go
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from 6182238 to 22ec0ba Compare September 9, 2026 15:23
@craciunoiuc
craciunoiuc requested a review from jedevc September 9, 2026 15:29
@craciunoiuc
craciunoiuc marked this pull request as ready for review September 9, 2026 15:29
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from 22ec0ba to 227f40c Compare September 9, 2026 15:29
RunResources worked the fields to create out inline, so nothing
outside could read them or add to them. A command needing a value
before the create had to build the patch spec a second time, which
runs any editor mode twice and hands the create different fields.

The work moves into 'resolve', which keeps what it worked out, and
'CreateFields' and 'SetCreateFields' read and replace the create
patches over that one resolution. Saving to a file stays ahead of
every editor, which the 'xor' tag on those flags guarantees.

Signed-off-by: Cezar Craciunoiu <cezar@unikraft.io>
Moving a service group onto a new image meant creating the new
instances, watching their states and deleting the old ones by hand,
with nothing to stop a half-finished swap from leaving the group
short or running two images at once.

The flag counts the group, creates that many instances in one
request through replicas, and deletes the old set only once every
new instance runs. A metro without room for both sets is refused
up front, and new instances that do not run are deleted again.

Signed-off-by: Cezar Craciunoiu <cezar@unikraft.io>
A rolling rollout runs both sets of instances at once, which a
workload holding sole access to its volume cannot do, and it
deletes the old set the moment the new one reports running, so an
instance that falls over straight after takes the group with it.

'--service-rollout=replace' stops the old instances and waits for
them before it starts the new ones, and starts the old set again
if the new one does not come up. '--rollout-healthy-after' holds
both modes up and rechecks them, and autoscale groups are refused.

Signed-off-by: Cezar Craciunoiu <cezar@unikraft.io>
@craciunoiuc
craciunoiuc force-pushed the craciunoiuc/add-rollout branch from 227f40c to 6882f9f Compare September 10, 2026 10:18
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