Skip to content

Commit f796b54

Browse files
committed
Add boot-from-volume support to server controller
Add support for booting servers from Cinder volumes instead of images. This enables the boot-from-volume (BFV) pattern where a bootable volume (created from an image) is used as the root disk. Design decisions: 1. Boot volume vs data volumes separation: - Only the boot volume (bootVolume field) is attached at server creation time via Nova's block device mapping - Additional data volumes continue to use the existing dynamic attachment mechanism (spec.resource.volumes) which attaches volumes after server creation - This separation allows data volumes to remain mutable (add/remove after server creation) while the boot volume is immutable - Avoids duplicating volume attachment logic between creation-time and runtime mechanisms 2. No deleteOnTermination option: - Deliberately not exposing Nova's delete_on_termination flag - If enabled, Nova would delete the underlying OpenStack volume when the server is deleted, but the ORC Volume resource would remain as an orphan - The orphaned Volume resource would then attempt to recreate the volume, leading to unexpected behavior - Users who want the volume deleted should delete both Server and Volume resources, maintaining consistent ORC resource lifecycle management API Changes: - Add ServerBootVolumeSpec type with volumeRef and optional tag fields - Add bootVolume field to ServerResourceSpec (mutually exclusive with imageRef) - Make imageRef optional (pointer) with CEL validation Controller Changes: - Add bootVolumeDependency with deletion guard and unique controller name - Handle boot-from-volume in CreateResource by building BlockDevice list Tests & Examples: - Add kuttl test for server boot-from-volume scenario - Add config/samples/openstack_v1alpha1_server_boot_from_volume.yaml - Add examples/bases/boot-from-volume/ with volume and server examples assisted-by: claude
1 parent f1dc06e commit f796b54

File tree

19 files changed

+525
-27
lines changed

19 files changed

+525
-27
lines changed

api/v1alpha1/server_types.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ type ServerPortSpec struct {
6060
PortRef *KubernetesNameRef `json:"portRef,omitempty"`
6161
}
6262

63+
// ServerBootVolumeSpec defines the boot volume for boot-from-volume server creation.
64+
// When specified, the server boots from this volume instead of an image.
65+
type ServerBootVolumeSpec struct {
66+
// volumeRef is a reference to a Volume object. The volume must be
67+
// bootable (created from an image) and available before server creation.
68+
// +required
69+
VolumeRef KubernetesNameRef `json:"volumeRef,omitempty"`
70+
71+
// tag is the device tag applied to the volume.
72+
// +kubebuilder:validation:MaxLength:=255
73+
// +optional
74+
Tag *string `json:"tag,omitempty"`
75+
}
76+
6377
// +kubebuilder:validation:MinProperties:=1
6478
type ServerVolumeSpec struct {
6579
// volumeRef is a reference to a Volume object. Server creation will wait for
@@ -122,23 +136,32 @@ type ServerInterfaceStatus struct {
122136
}
123137

124138
// ServerResourceSpec contains the desired state of a server
139+
// +kubebuilder:validation:XValidation:rule="has(self.imageRef) || has(self.bootVolume)",message="either imageRef or bootVolume must be specified"
140+
// +kubebuilder:validation:XValidation:rule="!(has(self.imageRef) && has(self.bootVolume))",message="imageRef and bootVolume are mutually exclusive"
125141
type ServerResourceSpec struct {
126142
// name will be the name of the created resource. If not specified, the
127143
// name of the ORC object will be used.
128144
// +optional
129145
Name *OpenStackName `json:"name,omitempty"`
130146

131147
// imageRef references the image to use for the server instance.
132-
// NOTE: This is not required in case of boot from volume.
133-
// +required
148+
// This field is required unless bootVolume is specified for boot-from-volume.
149+
// +optional
134150
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="imageRef is immutable"
135-
ImageRef KubernetesNameRef `json:"imageRef,omitempty"`
151+
ImageRef *KubernetesNameRef `json:"imageRef,omitempty"`
136152

137153
// flavorRef references the flavor to use for the server instance.
138154
// +required
139155
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="flavorRef is immutable"
140156
FlavorRef KubernetesNameRef `json:"flavorRef,omitempty"`
141157

158+
// bootVolume specifies a volume to boot from instead of an image.
159+
// When specified, imageRef must be omitted. The volume must be
160+
// bootable (created from an image using imageRef in the Volume spec).
161+
// +optional
162+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="bootVolume is immutable"
163+
BootVolume *ServerBootVolumeSpec `json:"bootVolume,omitempty"`
164+
142165
// userData specifies data which will be made available to the server at
143166
// boot time, either via the metadata service or a config drive. It is
144167
// typically read by a configuration service such as cloud-init or ignition.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/models-schema/zz_generated.openapi.go

Lines changed: 38 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/openstack.k-orc.cloud_servers.yaml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,29 @@ spec:
202202
x-kubernetes-validations:
203203
- message: availabilityZone is immutable
204204
rule: self == oldSelf
205+
bootVolume:
206+
description: |-
207+
bootVolume specifies a volume to boot from instead of an image.
208+
When specified, imageRef must be omitted. The volume must be
209+
bootable (created from an image using imageRef in the Volume spec).
210+
properties:
211+
tag:
212+
description: tag is the device tag applied to the volume.
213+
maxLength: 255
214+
type: string
215+
volumeRef:
216+
description: |-
217+
volumeRef is a reference to a Volume object. The volume must be
218+
bootable (created from an image) and available before server creation.
219+
maxLength: 253
220+
minLength: 1
221+
type: string
222+
required:
223+
- volumeRef
224+
type: object
225+
x-kubernetes-validations:
226+
- message: bootVolume is immutable
227+
rule: self == oldSelf
205228
configDrive:
206229
description: |-
207230
configDrive specifies whether to attach a config drive to the server.
@@ -223,7 +246,7 @@ spec:
223246
imageRef:
224247
description: |-
225248
imageRef references the image to use for the server instance.
226-
NOTE: This is not required in case of boot from volume.
249+
This field is required unless bootVolume is specified for boot-from-volume.
227250
maxLength: 253
228251
minLength: 1
229252
type: string
@@ -354,9 +377,13 @@ spec:
354377
x-kubernetes-list-type: atomic
355378
required:
356379
- flavorRef
357-
- imageRef
358380
- ports
359381
type: object
382+
x-kubernetes-validations:
383+
- message: either imageRef or bootVolume must be specified
384+
rule: has(self.imageRef) || has(self.bootVolume)
385+
- message: imageRef and bootVolume are mutually exclusive
386+
rule: '!(has(self.imageRef) && has(self.bootVolume))'
360387
required:
361388
- cloudCredentialsRef
362389
type: object
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Example of creating a server that boots from a Cinder volume instead of an image.
2+
# This is the boot-from-volume (BFV) pattern.
3+
#
4+
# Prerequisites:
5+
# - A bootable volume created from an image (see openstack_v1alpha1_volume_bootable.yaml)
6+
# - Network, subnet, and port resources
7+
# - A flavor
8+
---
9+
apiVersion: openstack.k-orc.cloud/v1alpha1
10+
kind: Server
11+
metadata:
12+
name: server-boot-from-volume-sample
13+
spec:
14+
cloudCredentialsRef:
15+
cloudName: openstack
16+
secretName: openstack-clouds
17+
managementPolicy: managed
18+
resource:
19+
# Note: No imageRef - booting from volume instead
20+
bootVolume:
21+
volumeRef: bootable-volume-sample
22+
flavorRef: server-sample
23+
ports:
24+
- portRef: server-sample
25+
availabilityZone: nova
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
apiVersion: kustomize.config.k8s.io/v1beta1
3+
kind: Kustomization
4+
resources:
5+
- volume.yaml
6+
- server.yaml
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
# Server that boots from a volume instead of an image
3+
apiVersion: openstack.k-orc.cloud/v1alpha1
4+
kind: Server
5+
metadata:
6+
name: server
7+
spec:
8+
cloudCredentialsRef:
9+
cloudName: openstack
10+
secretName: cloud-config
11+
managementPolicy: managed
12+
resource:
13+
# No imageRef - booting from volume
14+
bootVolume:
15+
volumeRef: boot-volume
16+
flavorRef: flavor
17+
ports:
18+
- portRef: port
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
# Bootable volume created from the cirros image
3+
apiVersion: openstack.k-orc.cloud/v1alpha1
4+
kind: Volume
5+
metadata:
6+
name: boot-volume
7+
spec:
8+
cloudCredentialsRef:
9+
cloudName: openstack
10+
secretName: cloud-config
11+
managementPolicy: managed
12+
resource:
13+
size: 1
14+
imageRef: cirros

internal/controllers/server/actuator.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,45 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp
160160

161161
reconcileStatus := progress.NewReconcileStatus()
162162

163-
var image *orcv1alpha1.Image
164-
{
163+
// Determine if we're booting from volume or image
164+
bootFromVolume := resource.BootVolume != nil
165+
166+
var imageID string
167+
if !bootFromVolume {
168+
// Traditional boot from image
165169
dep, imageReconcileStatus := imageDependency.GetDependency(
166170
ctx, actuator.k8sClient, obj, func(image *orcv1alpha1.Image) bool {
167171
return orcv1alpha1.IsAvailable(image) && image.Status.ID != nil
168172
},
169173
)
170174
reconcileStatus = reconcileStatus.WithReconcileStatus(imageReconcileStatus)
171-
image = dep
175+
if dep != nil && dep.Status.ID != nil {
176+
imageID = *dep.Status.ID
177+
}
178+
}
179+
180+
// Resolve boot volume for boot-from-volume
181+
var blockDevices []servers.BlockDevice
182+
if bootFromVolume {
183+
bootVolume, bvReconcileStatus := bootVolumeDependency.GetDependency(
184+
ctx, actuator.k8sClient, obj, func(volume *orcv1alpha1.Volume) bool {
185+
return orcv1alpha1.IsAvailable(volume) && volume.Status.ID != nil
186+
},
187+
)
188+
reconcileStatus = reconcileStatus.WithReconcileStatus(bvReconcileStatus)
189+
190+
if bootVolume != nil && bootVolume.Status.ID != nil {
191+
bd := servers.BlockDevice{
192+
SourceType: servers.SourceVolume,
193+
DestinationType: servers.DestinationVolume,
194+
UUID: *bootVolume.Status.ID,
195+
BootIndex: 0, // Always 0 for boot volume
196+
}
197+
if resource.BootVolume.Tag != nil {
198+
bd.Tag = *resource.BootVolume.Tag
199+
}
200+
blockDevices = append(blockDevices, bd)
201+
}
172202
}
173203

174204
flavor, flavorReconcileStatus := dependency.FetchDependency(
@@ -256,14 +286,15 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp
256286

257287
serverCreateOpts := servers.CreateOpts{
258288
Name: getResourceName(obj),
259-
ImageRef: *image.Status.ID,
289+
ImageRef: imageID, // Empty string if boot-from-volume
260290
FlavorRef: *flavor.Status.ID,
261291
Networks: portList,
262292
UserData: userData,
263293
Tags: tags,
264294
Metadata: metadata,
265295
AvailabilityZone: resource.AvailabilityZone,
266296
ConfigDrive: resource.ConfigDrive,
297+
BlockDevice: blockDevices, // Boot volume for BFV
267298
}
268299

269300
/* keypairs.CreateOptsExt was merged into servers.CreateOpts in gopher cloud V3

0 commit comments

Comments
 (0)