7.4 KiB
Plan: Implement CreateImage API
Context
Users need the ability to create a new AMI from a running (or stopped) instance so they can launch new instances from snapshots of existing ones. This is the standard AWS CreateImage flow: snapshot the instance's root volume, then register a new AMI pointing at that snapshot. All the building blocks exist (CreateSnapshot, AMIMetadata, OpenFromSnapshot, cloneAMIToVolume) — they just need to be wired together.
Flow
AWS CLI: create-image --instance-id i-xxx --name "my-image"
→ Gateway (ec2.go)
→ NATS "ec2.CreateImage"
→ Daemon handler
→ Finds instance → gets root volume ID
→ Calls snapshotService.CreateSnapshot(volumeID)
→ Creates new AMI viperblock config with AMIMetadata.SnapshotID
→ Stores as ami-xxx/config.json in S3
→ Returns ami-xxx
Changes
1. Gateway: Add CreateImage action entry
File: hive/gateway/ec2.go
- Add
"CreateImage"to theec2Actionsmap, following the DescribeImages pattern
2. Gateway: CreateImage handler
File: hive/gateway/ec2/image/CreateImage.go (new)
ValidateCreateImageInput()— validate InstanceId is present and starts withi-CreateImage()— create NATSImageService, callsvc.CreateImage(input)- Follow exact pattern of
gateway/ec2/snapshot/CreateSnapshot.go
3. NATS service: Wire CreateImage to NATS
File: hive/handlers/ec2/image/service_nats.go
- Replace the
CreateImagestub with a real NATS request to"ec2.CreateImage"topic - Use
utils.NATSRequest[ec2.CreateImageOutput]with 120s timeout (snapshot can be slow)
4. Daemon: Add NATS subscription and handler
File: hive/daemon/daemon.go
- Add
{"ec2.CreateImage", d.handleEC2CreateImage, "hive-workers"}to subscriptions - Update
NewImageServiceImpl(d.config)→NewImageServiceImpl(d.config, d.natsConn)(~line 515)
File: hive/daemon/daemon_handlers.go
- Add
handleEC2CreateImage— this is a stateful handler (NOT handleNATSRequest) because it needs instance state to find the root volume - Steps:
- Unmarshal
ec2.CreateImageInput - Lock
d.Instances.Mu, find instance ind.Instances.VMS[*input.InstanceId], unlock - Validate instance exists and is in running or stopped state
- Find root volume ID from
instance.Instance.BlockDeviceMappings(first entry withEbs.VolumeId) - Get instance status (running vs stopped) — pass to service so it knows which snapshot path to use
- Get source AMI ID from
instance.Instance.ImageId(for architecture, platform, etc.) - Call
d.imageService.CreateImageFromInstance(params)passing the extracted data - Respond with JSON
- Unmarshal
5. Image service: Update interface and add CreateImageParams
File: hive/handlers/ec2/image/service.go
- Keep
CreateImage(input *ec2.CreateImageInput)signature unchanged on the interface (gateway NATS side doesn't use extra params)
File: hive/handlers/ec2/image/service_impl.go
- Add a
CreateImageParamsstruct for the daemon-side call:type CreateImageParams struct { Input *ec2.CreateImageInput RootVolumeID string SourceImageID string IsRunning bool // true = use ebs.snapshot NATS, false = snapshot from S3 state } - The daemon handler calls
d.imageService.CreateImageFromInstance(params)directly (not through the generic interface)
6. Image service: Implement CreateImageFromInstance
File: hive/handlers/ec2/image/service_impl.go
Add natsConn *nats.Conn to ImageServiceImpl (for running-instance NATS snapshot calls and offline viperblock instantiation for stopped instances). Update constructor.
Implement CreateImageFromInstance(params CreateImageParams):
- Generate AMI ID:
utils.GenerateResourceID("ami") - Generate snapshot ID:
utils.GenerateResourceID("snap") - Snapshot the root volume — two paths:
- Running instance: NATS
ebs.snapshot.{volumeID}— reuse the exact pattern fromsnapshot/service_impl.go:191-218:- Marshal
config.EBSSnapshotRequest{Volume: rootVolumeID, SnapshotID: snapshotID} - Send NATS request to
ebs.snapshot.{rootVolumeID}with 30s timeout - Validate response
- Marshal
- Stopped instance: Volume is not mounted in viperblockd. Data is fully persisted in S3. Create snapshot offline:
- Instantiate viperblock via
newViperblock(rootVolumeID, ...)(reuse pattern frominstance/service_impl.go:238-258) backend.Init()+LoadStateRequest("")to load config + block map from S3- Call
vb.CreateSnapshot(snapshotID)directly — the flush/WAL steps are no-ops on a freshly loaded instance, it just serializes the block map into the snapshot checkpoint vb.RemoveLocalFiles()to clean up temp files
- Instantiate viperblock via
- Running instance: NATS
- Store snapshot metadata in
{snapshotID}/metadata.json— same pattern assnapshot/service_impl.go:220-252 - Read source AMI config from S3
{sourceImageID}/config.jsonto get architecture, platform, etc. - Read root volume config from S3
{rootVolumeID}/config.jsonto get volume size - Build AMI VolumeConfig with populated
AMIMetadata:ImageID: new ami-xxxName: from inputDescription: from inputSnapshotID: the snap-xxx just createdArchitecture,PlatformDetails,Virtualization, etc.: from source AMI (or defaults)VolumeSizeGiB: from root volume configCreationDate: nowRootDeviceType: "ebs"ImageOwnerAlias: "self"
- Store AMI config to S3 as
{amiID}/config.json— create aVBStatewith the VolumeConfig. This is whatDescribeImagesreads. - Return
&ec2.CreateImageOutput{ImageId: &amiID}
7. Tests
- Add unit test for
CreateImageinhive/handlers/ec2/image/with mock object store - Add gateway validation test in
hive/gateway/ec2/image/
Key Files
| File | Change |
|---|---|
hive/gateway/ec2.go |
Add CreateImage action |
hive/gateway/ec2/image/CreateImage.go |
New gateway handler |
hive/handlers/ec2/image/service.go |
Update interface |
hive/handlers/ec2/image/service_nats.go |
Wire NATS request |
hive/handlers/ec2/image/service_impl.go |
Core implementation |
hive/daemon/daemon.go |
Add subscription, update constructor |
hive/daemon/daemon_handlers.go |
Add stateful handler |
Existing Code to Reuse
snapshot/service_impl.go:191-218— EBS snapshot NATS pattern (ebs.snapshot.{volumeID})snapshot/service_impl.go:107-123—putSnapshotConfigS3 storage patternimage/service_impl.go:96-126— Reading viperblock VBState from S3 (config.json)instance/service_impl.go:238-258—newViperblock()for offline viperblock instantiationutils.GenerateResourceID()— ID generationutils.NATSRequest[T]()— Generic NATS request helperviperblock.VBState,viperblock.VolumeConfig,viperblock.AMIMetadata— Existing types
Verification
make build— confirm compilationmake test— all existing tests pass- Manual test:
# Launch instance from existing AMI aws ec2 run-instances --image-id ami-xxx --instance-type t2.micro --key-name mykey # Create image from running instance aws ec2 create-image --instance-id i-xxx --name "test-snapshot-image" # Verify it appears aws ec2 describe-images --owners self # Launch new instance from the snapshot-based AMI aws ec2 run-instances --image-id ami-yyy --instance-type t2.micro --key-name mykey