From 8ad865657461b68cf2ee4498e73a32e657bf558f Mon Sep 17 00:00:00 2001 From: Dale Henries Date: Fri, 13 Mar 2026 10:47:11 -0400 Subject: [PATCH 1/2] Add block device mapping support for Server resource Add boot-time block device mapping (BDM) to the Server resource, enabling boot-from-volume and custom disk configurations at server creation time. This complements the existing post-creation volume attachment via the volumes field. Changes: - Add ServerBlockDeviceSpec type with sourceType (volume/image/blank), volumeRef, imageRef, bootIndex, volumeSizeGiB, destinationType, deleteOnTermination, diskBus, deviceType, volumeType, and tag fields - Add blockDevices field to ServerResourceSpec (immutable after creation) - Make imageRef optional (pointer) to support boot-from-volume without a top-level image reference - Add CEL validations: require either imageRef or a blockDevice with bootIndex 0; enforce sourceType/ref consistency - Add bdmVolumeDependency and bdmImageDependency with deletion guards - Wire BDM dependencies into SetupWithManager watches - Resolve BDM dependencies and build BlockDevice slice in CreateResource - Regenerate deepcopy, apply configurations, CRDs, and docs --- api/v1alpha1/server_types.go | 101 ++++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 72 ++++++++++ cmd/models-schema/zz_generated.openapi.go | 120 +++++++++++++++- .../bases/openstack.k-orc.cloud_servers.yaml | 115 ++++++++++++++- internal/controllers/server/actuator.go | 71 +++++++++- internal/controllers/server/controller.go | 60 +++++++- .../api/v1alpha1/serverblockdevicespec.go | 133 ++++++++++++++++++ .../api/v1alpha1/serverresourcespec.go | 38 +++-- .../applyconfiguration/internal/internal.go | 44 ++++++ pkg/clients/applyconfiguration/utils.go | 2 + website/docs/crd-reference.md | 69 ++++++++- 11 files changed, 796 insertions(+), 29 deletions(-) create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/serverblockdevicespec.go diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go index f4f40b279..37583839a 100644 --- a/api/v1alpha1/server_types.go +++ b/api/v1alpha1/server_types.go @@ -60,6 +60,89 @@ type ServerPortSpec struct { PortRef *KubernetesNameRef `json:"portRef,omitempty"` } +// +kubebuilder:validation:Enum:=volume;image;blank +type BlockDeviceSourceType string + +const ( + BlockDeviceSourceTypeVolume BlockDeviceSourceType = "volume" + BlockDeviceSourceTypeImage BlockDeviceSourceType = "image" + BlockDeviceSourceTypeBlank BlockDeviceSourceType = "blank" +) + +// +kubebuilder:validation:Enum:=volume;local +type BlockDeviceDestinationType string + +const ( + BlockDeviceDestinationTypeVolume BlockDeviceDestinationType = "volume" + BlockDeviceDestinationTypeLocal BlockDeviceDestinationType = "local" +) + +// +kubebuilder:validation:XValidation:rule="self.sourceType == 'volume' ? has(self.volumeRef) : !has(self.volumeRef)",message="volumeRef must be set when sourceType is 'volume', and must not be set otherwise" +// +kubebuilder:validation:XValidation:rule="self.sourceType == 'image' ? has(self.imageRef) : !has(self.imageRef)",message="imageRef must be set when sourceType is 'image', and must not be set otherwise" +// +kubebuilder:validation:XValidation:rule="self.sourceType in ['image', 'blank'] ? has(self.volumeSizeGiB) : true",message="volumeSizeGiB is required when sourceType is 'image' or 'blank'" +type ServerBlockDeviceSpec struct { + // sourceType must be one of: "volume", "image", or "blank". + // +required + SourceType BlockDeviceSourceType `json:"sourceType"` + + // volumeRef is a reference to an ORC Volume object. Required when + // sourceType is "volume". + // +optional + VolumeRef *KubernetesNameRef `json:"volumeRef,omitempty"` + + // imageRef is a reference to an ORC Image object. Required when + // sourceType is "image". + // +optional + ImageRef *KubernetesNameRef `json:"imageRef,omitempty"` + + // bootIndex is the boot index of the device. Use 0 for the boot device. + // Use -1 for a non-bootable device. + // +kubebuilder:validation:Minimum:=-1 + // +required + BootIndex int32 `json:"bootIndex"` + + // volumeSizeGiB is the size of the volume to create (in gibibytes). + // Required when sourceType is "image" or "blank". + // +kubebuilder:validation:Minimum:=1 + // +optional + VolumeSizeGiB *int32 `json:"volumeSizeGiB,omitempty"` + + // destinationType is the type of device created. Possible values are + // "volume" and "local". Defaults to "volume". + // +optional + DestinationType *BlockDeviceDestinationType `json:"destinationType,omitempty"` + + // deleteOnTermination specifies whether or not to delete the + // attached volume when the server is deleted. Defaults to false. + // +optional + DeleteOnTermination *bool `json:"deleteOnTermination,omitempty"` + + // diskBus is the bus type of the block device. + // Examples: "virtio", "scsi", "ide", "usb". + // +kubebuilder:validation:MaxLength:=255 + // +optional + DiskBus *string `json:"diskBus,omitempty"` + + // deviceType specifies the device type of the block device. + // Examples: "disk", "cdrom", "floppy". + // +kubebuilder:validation:MaxLength:=255 + // +optional + DeviceType *string `json:"deviceType,omitempty"` + + // volumeType is the volume type to use when creating a volume. + // Only applicable when destinationType is "volume". + // +kubebuilder:validation:MaxLength:=255 + // +optional + VolumeType *string `json:"volumeType,omitempty"` + + // tag is an arbitrary string that can be applied to a block device. + // Information about the device tags can be obtained from the metadata API + // and the config drive. + // +kubebuilder:validation:MaxLength:=255 + // +optional + Tag *string `json:"tag,omitempty"` +} + // +kubebuilder:validation:MinProperties:=1 type ServerVolumeSpec struct { // volumeRef is a reference to a Volume object. Server creation will wait for @@ -122,6 +205,7 @@ type ServerInterfaceStatus struct { } // ServerResourceSpec contains the desired state of a server +// +kubebuilder:validation:XValidation:rule="has(self.imageRef) || (has(self.blockDevices) && self.blockDevices.exists(bd, bd.bootIndex == 0))",message="either imageRef or a blockDevice with bootIndex 0 must be specified" type ServerResourceSpec struct { // name will be the name of the created resource. If not specified, the // name of the ORC object will be used. @@ -129,10 +213,10 @@ type ServerResourceSpec struct { Name *OpenStackName `json:"name,omitempty"` // imageRef references the image to use for the server instance. - // NOTE: This is not required in case of boot from volume. - // +required + // This is not required when booting from a block device. + // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="imageRef is immutable" - ImageRef KubernetesNameRef `json:"imageRef,omitempty"` + ImageRef *KubernetesNameRef `json:"imageRef,omitempty"` // flavorRef references the flavor to use for the server instance. // +required @@ -152,12 +236,21 @@ type ServerResourceSpec struct { // +required Ports []ServerPortSpec `json:"ports,omitempty"` - // volumes is a list of volumes attached to the server. + // volumes is a list of volumes attached to the server after creation. // +kubebuilder:validation:MaxItems:=64 // +listType=atomic // +optional Volumes []ServerVolumeSpec `json:"volumes,omitempty"` + // blockDevices defines the block device mapping for the server at boot + // time. This controls how the server's disks are set up, including boot + // from volume. This is immutable after creation. + // +kubebuilder:validation:MaxItems:=64 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="blockDevices is immutable" + // +listType=atomic + // +optional + BlockDevices []ServerBlockDeviceSpec `json:"blockDevices,omitempty"` + // serverGroupRef is a reference to a ServerGroup object. The server // will be created in the server group. // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2b3b6c381..6621aa071 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4066,6 +4066,66 @@ func (in *Server) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerBlockDeviceSpec) DeepCopyInto(out *ServerBlockDeviceSpec) { + *out = *in + if in.VolumeRef != nil { + in, out := &in.VolumeRef, &out.VolumeRef + *out = new(KubernetesNameRef) + **out = **in + } + if in.ImageRef != nil { + in, out := &in.ImageRef, &out.ImageRef + *out = new(KubernetesNameRef) + **out = **in + } + if in.VolumeSizeGiB != nil { + in, out := &in.VolumeSizeGiB, &out.VolumeSizeGiB + *out = new(int32) + **out = **in + } + if in.DestinationType != nil { + in, out := &in.DestinationType, &out.DestinationType + *out = new(BlockDeviceDestinationType) + **out = **in + } + if in.DeleteOnTermination != nil { + in, out := &in.DeleteOnTermination, &out.DeleteOnTermination + *out = new(bool) + **out = **in + } + if in.DiskBus != nil { + in, out := &in.DiskBus, &out.DiskBus + *out = new(string) + **out = **in + } + if in.DeviceType != nil { + in, out := &in.DeviceType, &out.DeviceType + *out = new(string) + **out = **in + } + if in.VolumeType != nil { + in, out := &in.VolumeType, &out.VolumeType + *out = new(string) + **out = **in + } + if in.Tag != nil { + in, out := &in.Tag, &out.Tag + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerBlockDeviceSpec. +func (in *ServerBlockDeviceSpec) DeepCopy() *ServerBlockDeviceSpec { + if in == nil { + return nil + } + out := new(ServerBlockDeviceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerFilter) DeepCopyInto(out *ServerFilter) { *out = *in @@ -4484,6 +4544,11 @@ func (in *ServerResourceSpec) DeepCopyInto(out *ServerResourceSpec) { *out = new(OpenStackName) **out = **in } + if in.ImageRef != nil { + in, out := &in.ImageRef, &out.ImageRef + *out = new(KubernetesNameRef) + **out = **in + } if in.UserData != nil { in, out := &in.UserData, &out.UserData *out = new(UserDataSpec) @@ -4503,6 +4568,13 @@ func (in *ServerResourceSpec) DeepCopyInto(out *ServerResourceSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.BlockDevices != nil { + in, out := &in.BlockDevices, &out.BlockDevices + *out = make([]ServerBlockDeviceSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.ServerGroupRef != nil { in, out := &in.ServerGroupRef, &out.ServerGroupRef *out = new(KubernetesNameRef) diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index ee55bc2bb..c1f9a2430 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -169,6 +169,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SecurityGroupSpec": schema_openstack_resource_controller_v2_api_v1alpha1_SecurityGroupSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SecurityGroupStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SecurityGroupStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Server": schema_openstack_resource_controller_v2_api_v1alpha1_Server(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerBlockDeviceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ServerBlockDeviceSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerFilter": schema_openstack_resource_controller_v2_api_v1alpha1_ServerFilter(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerGroup": schema_openstack_resource_controller_v2_api_v1alpha1_ServerGroup(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerGroupFilter": schema_openstack_resource_controller_v2_api_v1alpha1_ServerGroupFilter(ref), @@ -7893,6 +7894,98 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_Server(ref common.Refe } } +func schema_openstack_resource_controller_v2_api_v1alpha1_ServerBlockDeviceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "sourceType": { + SchemaProps: spec.SchemaProps{ + Description: "sourceType must be one of: \"volume\", \"image\", or \"blank\".", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "volumeRef": { + SchemaProps: spec.SchemaProps{ + Description: "volumeRef is a reference to an ORC Volume object. Required when sourceType is \"volume\".", + Type: []string{"string"}, + Format: "", + }, + }, + "imageRef": { + SchemaProps: spec.SchemaProps{ + Description: "imageRef is a reference to an ORC Image object. Required when sourceType is \"image\".", + Type: []string{"string"}, + Format: "", + }, + }, + "bootIndex": { + SchemaProps: spec.SchemaProps{ + Description: "bootIndex is the boot index of the device. Use 0 for the boot device. Use -1 for a non-bootable device.", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + "volumeSizeGiB": { + SchemaProps: spec.SchemaProps{ + Description: "volumeSizeGiB is the size of the volume to create (in gibibytes). Required when sourceType is \"image\" or \"blank\".", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "destinationType": { + SchemaProps: spec.SchemaProps{ + Description: "destinationType is the type of device created. Possible values are \"volume\" and \"local\". Defaults to \"volume\".", + Type: []string{"string"}, + Format: "", + }, + }, + "deleteOnTermination": { + SchemaProps: spec.SchemaProps{ + Description: "deleteOnTermination specifies whether or not to delete the attached volume when the server is deleted. Defaults to false.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "diskBus": { + SchemaProps: spec.SchemaProps{ + Description: "diskBus is the bus type of the block device. Examples: \"virtio\", \"scsi\", \"ide\", \"usb\".", + Type: []string{"string"}, + Format: "", + }, + }, + "deviceType": { + SchemaProps: spec.SchemaProps{ + Description: "deviceType specifies the device type of the block device. Examples: \"disk\", \"cdrom\", \"floppy\".", + Type: []string{"string"}, + Format: "", + }, + }, + "volumeType": { + SchemaProps: spec.SchemaProps{ + Description: "volumeType is the volume type to use when creating a volume. Only applicable when destinationType is \"volume\".", + Type: []string{"string"}, + Format: "", + }, + }, + "tag": { + SchemaProps: spec.SchemaProps{ + Description: "tag is an arbitrary string that can be applied to a block device. Information about the device tags can be obtained from the metadata API and the config drive.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"sourceType", "bootIndex"}, + }, + }, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_ServerFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -8629,7 +8722,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref }, "imageRef": { SchemaProps: spec.SchemaProps{ - Description: "imageRef references the image to use for the server instance. NOTE: This is not required in case of boot from volume.", + Description: "imageRef references the image to use for the server instance. This is not required when booting from a block device.", Type: []string{"string"}, Format: "", }, @@ -8673,7 +8766,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref }, }, SchemaProps: spec.SchemaProps{ - Description: "volumes is a list of volumes attached to the server.", + Description: "volumes is a list of volumes attached to the server after creation.", Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ @@ -8685,6 +8778,25 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref }, }, }, + "blockDevices": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "blockDevices defines the block device mapping for the server at boot time. This controls how the server's disks are set up, including boot from volume. This is immutable after creation.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerBlockDeviceSpec"), + }, + }, + }, + }, + }, "serverGroupRef": { SchemaProps: spec.SchemaProps{ Description: "serverGroupRef is a reference to a ServerGroup object. The server will be created in the server group.", @@ -8753,11 +8865,11 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref }, }, }, - Required: []string{"imageRef", "flavorRef", "ports"}, + Required: []string{"flavorRef", "ports"}, }, }, Dependencies: []string{ - "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerMetadata", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerPortSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerVolumeSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.UserDataSpec"}, + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerBlockDeviceSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerMetadata", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerPortSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerVolumeSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.UserDataSpec"}, } } diff --git a/config/crd/bases/openstack.k-orc.cloud_servers.yaml b/config/crd/bases/openstack.k-orc.cloud_servers.yaml index 8387dd81c..01e5e8ac2 100644 --- a/config/crd/bases/openstack.k-orc.cloud_servers.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_servers.yaml @@ -203,6 +203,110 @@ spec: x-kubernetes-validations: - message: availabilityZone is immutable rule: self == oldSelf + blockDevices: + description: |- + blockDevices defines the block device mapping for the server at boot + time. This controls how the server's disks are set up, including boot + from volume. This is immutable after creation. + items: + properties: + bootIndex: + description: |- + bootIndex is the boot index of the device. Use 0 for the boot device. + Use -1 for a non-bootable device. + format: int32 + minimum: -1 + type: integer + deleteOnTermination: + description: |- + deleteOnTermination specifies whether or not to delete the + attached volume when the server is deleted. Defaults to false. + type: boolean + destinationType: + description: |- + destinationType is the type of device created. Possible values are + "volume" and "local". Defaults to "volume". + enum: + - volume + - local + type: string + deviceType: + description: |- + deviceType specifies the device type of the block device. + Examples: "disk", "cdrom", "floppy". + maxLength: 255 + type: string + diskBus: + description: |- + diskBus is the bus type of the block device. + Examples: "virtio", "scsi", "ide", "usb". + maxLength: 255 + type: string + imageRef: + description: |- + imageRef is a reference to an ORC Image object. Required when + sourceType is "image". + maxLength: 253 + minLength: 1 + type: string + sourceType: + description: 'sourceType must be one of: "volume", "image", + or "blank".' + enum: + - volume + - image + - blank + type: string + tag: + description: |- + tag is an arbitrary string that can be applied to a block device. + Information about the device tags can be obtained from the metadata API + and the config drive. + maxLength: 255 + type: string + volumeRef: + description: |- + volumeRef is a reference to an ORC Volume object. Required when + sourceType is "volume". + maxLength: 253 + minLength: 1 + type: string + volumeSizeGiB: + description: |- + volumeSizeGiB is the size of the volume to create (in gibibytes). + Required when sourceType is "image" or "blank". + format: int32 + minimum: 1 + type: integer + volumeType: + description: |- + volumeType is the volume type to use when creating a volume. + Only applicable when destinationType is "volume". + maxLength: 255 + type: string + required: + - bootIndex + - sourceType + type: object + x-kubernetes-validations: + - message: volumeRef must be set when sourceType is 'volume', + and must not be set otherwise + rule: 'self.sourceType == ''volume'' ? has(self.volumeRef) + : !has(self.volumeRef)' + - message: imageRef must be set when sourceType is 'image', + and must not be set otherwise + rule: 'self.sourceType == ''image'' ? has(self.imageRef) : + !has(self.imageRef)' + - message: volumeSizeGiB is required when sourceType is 'image' + or 'blank' + rule: 'self.sourceType in [''image'', ''blank''] ? has(self.volumeSizeGiB) + : true' + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: blockDevices is immutable + rule: self == oldSelf configDrive: description: |- configDrive specifies whether to attach a config drive to the server. @@ -224,7 +328,7 @@ spec: imageRef: description: |- imageRef references the image to use for the server instance. - NOTE: This is not required in case of boot from volume. + This is not required when booting from a block device. maxLength: 253 minLength: 1 type: string @@ -330,7 +434,8 @@ spec: - message: userData is immutable rule: self == oldSelf volumes: - description: volumes is a list of volumes attached to the server. + description: volumes is a list of volumes attached to the server + after creation. items: minProperties: 1 properties: @@ -355,9 +460,13 @@ spec: x-kubernetes-list-type: atomic required: - flavorRef - - imageRef - ports type: object + x-kubernetes-validations: + - message: either imageRef or a blockDevice with bootIndex 0 must + be specified + rule: has(self.imageRef) || (has(self.blockDevices) && self.blockDevices.exists(bd, + bd.bootIndex == 0)) required: - cloudCredentialsRef type: object diff --git a/internal/controllers/server/actuator.go b/internal/controllers/server/actuator.go index a044503f6..d632895b5 100644 --- a/internal/controllers/server/actuator.go +++ b/internal/controllers/server/actuator.go @@ -161,7 +161,7 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp reconcileStatus := progress.NewReconcileStatus() var image *orcv1alpha1.Image - { + if resource.ImageRef != nil { dep, imageReconcileStatus := imageDependency.GetDependency( ctx, actuator.k8sClient, obj, func(image *orcv1alpha1.Image) bool { return orcv1alpha1.IsAvailable(image) && image.Status.ID != nil @@ -238,6 +238,28 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp } } + var bdmVolumesMap map[string]*orcv1alpha1.Volume + if len(resource.BlockDevices) > 0 { + bdmVols, bdmVolReconcileStatus := bdmVolumeDependency.GetDependencies( + ctx, actuator.k8sClient, obj, func(vol *orcv1alpha1.Volume) bool { + return orcv1alpha1.IsAvailable(vol) && vol.Status.ID != nil + }, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(bdmVolReconcileStatus) + bdmVolumesMap = bdmVols + } + + var bdmImagesMap map[string]*orcv1alpha1.Image + if len(resource.BlockDevices) > 0 { + bdmImgs, bdmImgReconcileStatus := bdmImageDependency.GetDependencies( + ctx, actuator.k8sClient, obj, func(img *orcv1alpha1.Image) bool { + return orcv1alpha1.IsAvailable(img) && img.Status.ID != nil + }, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(bdmImgReconcileStatus) + bdmImagesMap = bdmImgs + } + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { return nil, reconcileStatus } @@ -254,9 +276,50 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp metadata[m.Key] = m.Value } + var blockDevices []servers.BlockDevice + for i := range resource.BlockDevices { + bdSpec := &resource.BlockDevices[i] + bd := servers.BlockDevice{ + SourceType: servers.SourceType(bdSpec.SourceType), + BootIndex: int(bdSpec.BootIndex), + DeleteOnTermination: ptr.Deref(bdSpec.DeleteOnTermination, false), + } + + if bdSpec.DestinationType != nil { + bd.DestinationType = servers.DestinationType(*bdSpec.DestinationType) + } + if bdSpec.VolumeSizeGiB != nil { + bd.VolumeSize = int(*bdSpec.VolumeSizeGiB) + } + if bdSpec.DiskBus != nil { + bd.DiskBus = *bdSpec.DiskBus + } + if bdSpec.DeviceType != nil { + bd.DeviceType = *bdSpec.DeviceType + } + if bdSpec.VolumeType != nil { + bd.VolumeType = *bdSpec.VolumeType + } + if bdSpec.Tag != nil { + bd.Tag = *bdSpec.Tag + } + + switch bdSpec.SourceType { + case orcv1alpha1.BlockDeviceSourceTypeVolume: + vol := bdmVolumesMap[string(*bdSpec.VolumeRef)] + bd.UUID = *vol.Status.ID + case orcv1alpha1.BlockDeviceSourceTypeImage: + img := bdmImagesMap[string(*bdSpec.ImageRef)] + bd.UUID = *img.Status.ID + case orcv1alpha1.BlockDeviceSourceTypeBlank: + // No UUID needed + } + + blockDevices = append(blockDevices, bd) + } + serverCreateOpts := servers.CreateOpts{ Name: getResourceName(obj), - ImageRef: *image.Status.ID, FlavorRef: *flavor.Status.ID, Networks: portList, UserData: userData, @@ -264,6 +327,10 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp Metadata: metadata, AvailabilityZone: resource.AvailabilityZone, ConfigDrive: resource.ConfigDrive, + BlockDevice: blockDevices, + } + if image != nil && image.Status.ID != nil { + serverCreateOpts.ImageRef = *image.Status.ID } /* keypairs.CreateOptsExt was merged into servers.CreateOpts in gopher cloud V3 diff --git a/internal/controllers/server/controller.go b/internal/controllers/server/controller.go index 95ac9f595..ab24bee99 100644 --- a/internal/controllers/server/controller.go +++ b/internal/controllers/server/controller.go @@ -73,11 +73,11 @@ var ( "spec.resource.imageRef", func(server *orcv1alpha1.Server) []string { resource := server.Spec.Resource - if resource == nil { + if resource == nil || resource.ImageRef == nil { return nil } - return []string{string(resource.ImageRef)} + return []string{string(*resource.ImageRef)} }, finalizer, externalObjectFieldOwner, ) @@ -161,6 +161,46 @@ var ( }, finalizer, externalObjectFieldOwner, ) + + bdmVolumeDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.ServerList, *orcv1alpha1.Volume]( + "spec.resource.blockDevices.volumeRef", + func(server *orcv1alpha1.Server) []string { + resource := server.Spec.Resource + if resource == nil { + return nil + } + + refs := make([]string, 0, len(resource.BlockDevices)) + for i := range resource.BlockDevices { + bd := &resource.BlockDevices[i] + if bd.VolumeRef != nil { + refs = append(refs, string(*bd.VolumeRef)) + } + } + return refs + }, + finalizer, externalObjectFieldOwner, + ) + + bdmImageDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.ServerList, *orcv1alpha1.Image]( + "spec.resource.blockDevices.imageRef", + func(server *orcv1alpha1.Server) []string { + resource := server.Spec.Resource + if resource == nil { + return nil + } + + refs := make([]string, 0, len(resource.BlockDevices)) + for i := range resource.BlockDevices { + bd := &resource.BlockDevices[i] + if bd.ImageRef != nil { + refs = append(refs, string(*bd.ImageRef)) + } + } + return refs + }, + finalizer, externalObjectFieldOwner, + ) ) // SetupWithManager sets up the controller with the Manager. @@ -196,6 +236,14 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c if err != nil { return err } + bdmVolumeWatchEventHandler, err := bdmVolumeDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + bdmImageWatchEventHandler, err := bdmImageDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). @@ -218,6 +266,12 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c Watches(&orcv1alpha1.KeyPair{}, keypairWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.KeyPair{})), ). + Watches(&orcv1alpha1.Volume{}, bdmVolumeWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Volume{})), + ). + Watches(&orcv1alpha1.Image{}, bdmImageWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Image{})), + ). // XXX: This is a general watch on secrets. A general watch on secrets // is undesirable because: // - It requires problematic RBAC @@ -235,6 +289,8 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c userDataDependency.AddToManager(ctx, mgr), volumeDependency.AddToManager(ctx, mgr), keypairDependency.AddToManager(ctx, mgr), + bdmVolumeDependency.AddToManager(ctx, mgr), + bdmImageDependency.AddToManager(ctx, mgr), credentialsDependency.AddToManager(ctx, mgr), credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), ); err != nil { diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/serverblockdevicespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/serverblockdevicespec.go new file mode 100644 index 000000000..837257225 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/serverblockdevicespec.go @@ -0,0 +1,133 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" +) + +// ServerBlockDeviceSpecApplyConfiguration represents a declarative configuration of the ServerBlockDeviceSpec type for use +// with apply. +type ServerBlockDeviceSpecApplyConfiguration struct { + SourceType *apiv1alpha1.BlockDeviceSourceType `json:"sourceType,omitempty"` + VolumeRef *apiv1alpha1.KubernetesNameRef `json:"volumeRef,omitempty"` + ImageRef *apiv1alpha1.KubernetesNameRef `json:"imageRef,omitempty"` + BootIndex *int32 `json:"bootIndex,omitempty"` + VolumeSizeGiB *int32 `json:"volumeSizeGiB,omitempty"` + DestinationType *apiv1alpha1.BlockDeviceDestinationType `json:"destinationType,omitempty"` + DeleteOnTermination *bool `json:"deleteOnTermination,omitempty"` + DiskBus *string `json:"diskBus,omitempty"` + DeviceType *string `json:"deviceType,omitempty"` + VolumeType *string `json:"volumeType,omitempty"` + Tag *string `json:"tag,omitempty"` +} + +// ServerBlockDeviceSpecApplyConfiguration constructs a declarative configuration of the ServerBlockDeviceSpec type for use with +// apply. +func ServerBlockDeviceSpec() *ServerBlockDeviceSpecApplyConfiguration { + return &ServerBlockDeviceSpecApplyConfiguration{} +} + +// WithSourceType sets the SourceType field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SourceType field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithSourceType(value apiv1alpha1.BlockDeviceSourceType) *ServerBlockDeviceSpecApplyConfiguration { + b.SourceType = &value + return b +} + +// WithVolumeRef sets the VolumeRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the VolumeRef field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithVolumeRef(value apiv1alpha1.KubernetesNameRef) *ServerBlockDeviceSpecApplyConfiguration { + b.VolumeRef = &value + return b +} + +// WithImageRef sets the ImageRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ImageRef field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithImageRef(value apiv1alpha1.KubernetesNameRef) *ServerBlockDeviceSpecApplyConfiguration { + b.ImageRef = &value + return b +} + +// WithBootIndex sets the BootIndex field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BootIndex field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithBootIndex(value int32) *ServerBlockDeviceSpecApplyConfiguration { + b.BootIndex = &value + return b +} + +// WithVolumeSizeGiB sets the VolumeSizeGiB field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the VolumeSizeGiB field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithVolumeSizeGiB(value int32) *ServerBlockDeviceSpecApplyConfiguration { + b.VolumeSizeGiB = &value + return b +} + +// WithDestinationType sets the DestinationType field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DestinationType field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithDestinationType(value apiv1alpha1.BlockDeviceDestinationType) *ServerBlockDeviceSpecApplyConfiguration { + b.DestinationType = &value + return b +} + +// WithDeleteOnTermination sets the DeleteOnTermination field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeleteOnTermination field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithDeleteOnTermination(value bool) *ServerBlockDeviceSpecApplyConfiguration { + b.DeleteOnTermination = &value + return b +} + +// WithDiskBus sets the DiskBus field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DiskBus field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithDiskBus(value string) *ServerBlockDeviceSpecApplyConfiguration { + b.DiskBus = &value + return b +} + +// WithDeviceType sets the DeviceType field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeviceType field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithDeviceType(value string) *ServerBlockDeviceSpecApplyConfiguration { + b.DeviceType = &value + return b +} + +// WithVolumeType sets the VolumeType field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the VolumeType field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithVolumeType(value string) *ServerBlockDeviceSpecApplyConfiguration { + b.VolumeType = &value + return b +} + +// WithTag sets the Tag field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Tag field is set to the value of the last call. +func (b *ServerBlockDeviceSpecApplyConfiguration) WithTag(value string) *ServerBlockDeviceSpecApplyConfiguration { + b.Tag = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go index c3308477a..d1cc55de3 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go @@ -25,18 +25,19 @@ import ( // ServerResourceSpecApplyConfiguration represents a declarative configuration of the ServerResourceSpec type for use // with apply. type ServerResourceSpecApplyConfiguration struct { - Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` - ImageRef *apiv1alpha1.KubernetesNameRef `json:"imageRef,omitempty"` - FlavorRef *apiv1alpha1.KubernetesNameRef `json:"flavorRef,omitempty"` - UserData *UserDataSpecApplyConfiguration `json:"userData,omitempty"` - Ports []ServerPortSpecApplyConfiguration `json:"ports,omitempty"` - Volumes []ServerVolumeSpecApplyConfiguration `json:"volumes,omitempty"` - ServerGroupRef *apiv1alpha1.KubernetesNameRef `json:"serverGroupRef,omitempty"` - AvailabilityZone *string `json:"availabilityZone,omitempty"` - KeypairRef *apiv1alpha1.KubernetesNameRef `json:"keypairRef,omitempty"` - Tags []apiv1alpha1.ServerTag `json:"tags,omitempty"` - Metadata []ServerMetadataApplyConfiguration `json:"metadata,omitempty"` - ConfigDrive *bool `json:"configDrive,omitempty"` + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + ImageRef *apiv1alpha1.KubernetesNameRef `json:"imageRef,omitempty"` + FlavorRef *apiv1alpha1.KubernetesNameRef `json:"flavorRef,omitempty"` + UserData *UserDataSpecApplyConfiguration `json:"userData,omitempty"` + Ports []ServerPortSpecApplyConfiguration `json:"ports,omitempty"` + Volumes []ServerVolumeSpecApplyConfiguration `json:"volumes,omitempty"` + BlockDevices []ServerBlockDeviceSpecApplyConfiguration `json:"blockDevices,omitempty"` + ServerGroupRef *apiv1alpha1.KubernetesNameRef `json:"serverGroupRef,omitempty"` + AvailabilityZone *string `json:"availabilityZone,omitempty"` + KeypairRef *apiv1alpha1.KubernetesNameRef `json:"keypairRef,omitempty"` + Tags []apiv1alpha1.ServerTag `json:"tags,omitempty"` + Metadata []ServerMetadataApplyConfiguration `json:"metadata,omitempty"` + ConfigDrive *bool `json:"configDrive,omitempty"` } // ServerResourceSpecApplyConfiguration constructs a declarative configuration of the ServerResourceSpec type for use with @@ -103,6 +104,19 @@ func (b *ServerResourceSpecApplyConfiguration) WithVolumes(values ...*ServerVolu return b } +// WithBlockDevices adds the given value to the BlockDevices field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the BlockDevices field. +func (b *ServerResourceSpecApplyConfiguration) WithBlockDevices(values ...*ServerBlockDeviceSpecApplyConfiguration) *ServerResourceSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithBlockDevices") + } + b.BlockDevices = append(b.BlockDevices, *values[i]) + } + return b +} + // WithServerGroupRef sets the ServerGroupRef field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ServerGroupRef field is set to the value of the last call. diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 3a7e6ae0c..36b8c117d 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -2299,6 +2299,44 @@ var schemaYAML = typed.YAMLObject(`types: type: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerStatus default: {} +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerBlockDeviceSpec + map: + fields: + - name: bootIndex + type: + scalar: numeric + default: 0 + - name: deleteOnTermination + type: + scalar: boolean + - name: destinationType + type: + scalar: string + - name: deviceType + type: + scalar: string + - name: diskBus + type: + scalar: string + - name: imageRef + type: + scalar: string + - name: sourceType + type: + scalar: string + default: "" + - name: tag + type: + scalar: string + - name: volumeRef + type: + scalar: string + - name: volumeSizeGiB + type: + scalar: numeric + - name: volumeType + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerFilter map: fields: @@ -2515,6 +2553,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: availabilityZone type: scalar: string + - name: blockDevices + type: + list: + elementType: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerBlockDeviceSpec + elementRelationship: atomic - name: configDrive type: scalar: boolean diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index 0e7f2efb3..abaf574a7 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -282,6 +282,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.SecurityGroupStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Server"): return &apiv1alpha1.ServerApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ServerBlockDeviceSpec"): + return &apiv1alpha1.ServerBlockDeviceSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerFilter"): return &apiv1alpha1.ServerFilterApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerGroup"): diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 5512b3f6b..a92db3329 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -136,6 +136,43 @@ _Appears in:_ +#### BlockDeviceDestinationType + +_Underlying type:_ _string_ + + + +_Validation:_ +- Enum: [volume local] + +_Appears in:_ +- [ServerBlockDeviceSpec](#serverblockdevicespec) + +| Field | Description | +| --- | --- | +| `volume` | | +| `local` | | + + +#### BlockDeviceSourceType + +_Underlying type:_ _string_ + + + +_Validation:_ +- Enum: [volume image blank] + +_Appears in:_ +- [ServerBlockDeviceSpec](#serverblockdevicespec) + +| Field | Description | +| --- | --- | +| `volume` | | +| `image` | | +| `blank` | | + + #### CIDR _Underlying type:_ _string_ @@ -1793,6 +1830,7 @@ _Appears in:_ - [RouterResourceSpec](#routerresourcespec) - [SecurityGroupFilter](#securitygroupfilter) - [SecurityGroupResourceSpec](#securitygroupresourcespec) +- [ServerBlockDeviceSpec](#serverblockdevicespec) - [ServerPortSpec](#serverportspec) - [ServerResourceSpec](#serverresourcespec) - [ServerVolumeSpec](#servervolumespec) @@ -3218,6 +3256,32 @@ Server is the Schema for an ORC resource. | `status` _[ServerStatus](#serverstatus)_ | status defines the observed state of the resource. | | | +#### ServerBlockDeviceSpec + + + + + + + +_Appears in:_ +- [ServerResourceSpec](#serverresourcespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `sourceType` _[BlockDeviceSourceType](#blockdevicesourcetype)_ | sourceType must be one of: "volume", "image", or "blank". | | Enum: [volume image blank]
| +| `volumeRef` _[KubernetesNameRef](#kubernetesnameref)_ | volumeRef is a reference to an ORC Volume object. Required when
sourceType is "volume". | | MaxLength: 253
MinLength: 1
| +| `imageRef` _[KubernetesNameRef](#kubernetesnameref)_ | imageRef is a reference to an ORC Image object. Required when
sourceType is "image". | | MaxLength: 253
MinLength: 1
| +| `bootIndex` _integer_ | bootIndex is the boot index of the device. Use 0 for the boot device.
Use -1 for a non-bootable device. | | Minimum: -1
| +| `volumeSizeGiB` _integer_ | volumeSizeGiB is the size of the volume to create (in gibibytes).
Required when sourceType is "image" or "blank". | | Minimum: 1
| +| `destinationType` _[BlockDeviceDestinationType](#blockdevicedestinationtype)_ | destinationType is the type of device created. Possible values are
"volume" and "local". Defaults to "volume". | | Enum: [volume local]
| +| `deleteOnTermination` _boolean_ | deleteOnTermination specifies whether or not to delete the
attached volume when the server is deleted. Defaults to false. | | | +| `diskBus` _string_ | diskBus is the bus type of the block device.
Examples: "virtio", "scsi", "ide", "usb". | | MaxLength: 255
| +| `deviceType` _string_ | deviceType specifies the device type of the block device.
Examples: "disk", "cdrom", "floppy". | | MaxLength: 255
| +| `volumeType` _string_ | volumeType is the volume type to use when creating a volume.
Only applicable when destinationType is "volume". | | MaxLength: 255
| +| `tag` _string_ | tag is an arbitrary string that can be applied to a block device.
Information about the device tags can be obtained from the metadata API
and the config drive. | | MaxLength: 255
| + + #### ServerFilter @@ -3547,11 +3611,12 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _[OpenStackName](#openstackname)_ | name will be the name of the created resource. If not specified, the
name of the ORC object will be used. | | MaxLength: 255
MinLength: 1
Pattern: `^[^,]+$`
| -| `imageRef` _[KubernetesNameRef](#kubernetesnameref)_ | imageRef references the image to use for the server instance.
NOTE: This is not required in case of boot from volume. | | MaxLength: 253
MinLength: 1
| +| `imageRef` _[KubernetesNameRef](#kubernetesnameref)_ | imageRef references the image to use for the server instance.
This is not required when booting from a block device. | | MaxLength: 253
MinLength: 1
| | `flavorRef` _[KubernetesNameRef](#kubernetesnameref)_ | flavorRef references the flavor to use for the server instance. | | MaxLength: 253
MinLength: 1
| | `userData` _[UserDataSpec](#userdataspec)_ | userData specifies data which will be made available to the server at
boot time, either via the metadata service or a config drive. It is
typically read by a configuration service such as cloud-init or ignition. | | MaxProperties: 1
MinProperties: 1
| | `ports` _[ServerPortSpec](#serverportspec) array_ | ports defines a list of ports which will be attached to the server. | | MaxItems: 64
MaxProperties: 1
MinProperties: 1
| -| `volumes` _[ServerVolumeSpec](#servervolumespec) array_ | volumes is a list of volumes attached to the server. | | MaxItems: 64
MinProperties: 1
| +| `volumes` _[ServerVolumeSpec](#servervolumespec) array_ | volumes is a list of volumes attached to the server after creation. | | MaxItems: 64
MinProperties: 1
| +| `blockDevices` _[ServerBlockDeviceSpec](#serverblockdevicespec) array_ | blockDevices defines the block device mapping for the server at boot
time. This controls how the server's disks are set up, including boot
from volume. This is immutable after creation. | | MaxItems: 64
| | `serverGroupRef` _[KubernetesNameRef](#kubernetesnameref)_ | serverGroupRef is a reference to a ServerGroup object. The server
will be created in the server group. | | MaxLength: 253
MinLength: 1
| | `availabilityZone` _string_ | availabilityZone is the availability zone in which to create the server. | | MaxLength: 255
| | `keypairRef` _[KubernetesNameRef](#kubernetesnameref)_ | keypairRef is a reference to a KeyPair object. The server will be
created with this keypair for SSH access. | | MaxLength: 253
MinLength: 1
| From 94924ee36097e631c00b25bcb9bc64ffea63adc3 Mon Sep 17 00:00:00 2001 From: Dale Henries Date: Fri, 13 Mar 2026 10:47:49 -0400 Subject: [PATCH 2/2] Add KUTTL E2E tests for server block device mapping Add three test suites covering block device mapping functionality: - server-create-bdm: Boot from volume using image source with no top-level imageRef. Verifies server reaches ACTIVE with volume attached and correct network configuration. - server-create-bdm-volume: Boot from an existing ORC Volume. Creates a bootable volume from image, then references it in BDM with sourceType: volume. - server-bdm-dependency: Tests BDM dependency ordering and deletion guards. Verifies server waits for missing BDM image dependency, becomes available once created, and that the image has a deletion guard finalizer preventing premature deletion. --- .../server-bdm-dependency/00-assert.yaml | 15 +++++ .../00-create-server-without-image.yaml | 61 +++++++++++++++++++ .../server-bdm-dependency/00-secret.yaml | 8 +++ .../server-bdm-dependency/01-assert.yaml | 15 +++++ .../01-create-image.yaml | 16 +++++ .../server-bdm-dependency/02-assert.yaml | 14 +++++ .../02-delete-dependencies.yaml | 9 +++ .../server-bdm-dependency/03-assert.yaml | 7 +++ .../03-delete-server.yaml | 6 ++ .../server-create-bdm-volume/00-assert.yaml | 45 ++++++++++++++ .../00-create-resource.yaml | 46 ++++++++++++++ .../00-prerequisites.yaml | 50 +++++++++++++++ .../tests/server-create-bdm/00-assert.yaml | 41 +++++++++++++ .../server-create-bdm/00-create-resource.yaml | 35 +++++++++++ .../server-create-bdm/00-prerequisites.yaml | 50 +++++++++++++++ 15 files changed, 418 insertions(+) create mode 100644 internal/controllers/server/tests/server-bdm-dependency/00-assert.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/00-create-server-without-image.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/00-secret.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/01-assert.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/01-create-image.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/02-assert.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/02-delete-dependencies.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/03-assert.yaml create mode 100644 internal/controllers/server/tests/server-bdm-dependency/03-delete-server.yaml create mode 100644 internal/controllers/server/tests/server-create-bdm-volume/00-assert.yaml create mode 100644 internal/controllers/server/tests/server-create-bdm-volume/00-create-resource.yaml create mode 100644 internal/controllers/server/tests/server-create-bdm-volume/00-prerequisites.yaml create mode 100644 internal/controllers/server/tests/server-create-bdm/00-assert.yaml create mode 100644 internal/controllers/server/tests/server-create-bdm/00-create-resource.yaml create mode 100644 internal/controllers/server/tests/server-create-bdm/00-prerequisites.yaml diff --git a/internal/controllers/server/tests/server-bdm-dependency/00-assert.yaml b/internal/controllers/server/tests/server-bdm-dependency/00-assert.yaml new file mode 100644 index 000000000..7fd25bc42 --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/00-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-bdm-dependency +status: + conditions: + - type: Available + message: Waiting for Image/server-bdm-dependency to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for Image/server-bdm-dependency to be created + status: "True" + reason: Progressing diff --git a/internal/controllers/server/tests/server-bdm-dependency/00-create-server-without-image.yaml b/internal/controllers/server/tests/server-bdm-dependency/00-create-server-without-image.yaml new file mode 100644 index 000000000..e0ad167c5 --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/00-create-server-without-image.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Network +metadata: + name: server-bdm-dependency +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: server-bdm-dependency +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Subnet +metadata: + name: server-bdm-dependency +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-bdm-dependency + ipVersion: 4 + cidr: 192.168.200.0/24 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Port +metadata: + name: server-bdm-dependency +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-bdm-dependency + addresses: + - subnetRef: server-bdm-dependency +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-bdm-dependency +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + flavorRef: server-bdm-dependency + ports: + - portRef: server-bdm-dependency + blockDevices: + - sourceType: image + imageRef: server-bdm-dependency + bootIndex: 0 + volumeSizeGiB: 1 + destinationType: volume + deleteOnTermination: true diff --git a/internal/controllers/server/tests/server-bdm-dependency/00-secret.yaml b/internal/controllers/server/tests/server-bdm-dependency/00-secret.yaml new file mode 100644 index 000000000..f4ac31d62 --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/00-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - script: | + export E2E_KUTTL_CURRENT_TEST=server-bdm-dependency + cat ../templates/create-flavor.tmpl | envsubst | kubectl -n ${NAMESPACE} apply -f - diff --git a/internal/controllers/server/tests/server-bdm-dependency/01-assert.yaml b/internal/controllers/server/tests/server-bdm-dependency/01-assert.yaml new file mode 100644 index 000000000..3c379abed --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/01-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-bdm-dependency +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success diff --git a/internal/controllers/server/tests/server-bdm-dependency/01-create-image.yaml b/internal/controllers/server/tests/server-bdm-dependency/01-create-image.yaml new file mode 100644 index 000000000..3c226c13d --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/01-create-image.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: server-bdm-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + content: + diskFormat: qcow2 + download: + url: https://github.com/k-orc/openstack-resource-controller/raw/2ddc1857f5e22d2f0df6f5ee033353e4fd907121/internal/controllers/image/testdata/cirros-0.6.3-x86_64-disk.img + visibility: public diff --git a/internal/controllers/server/tests/server-bdm-dependency/02-assert.yaml b/internal/controllers/server/tests/server-bdm-dependency/02-assert.yaml new file mode 100644 index 000000000..933fd9c6a --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/02-assert.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Image + name: server-bdm-dependency + ref: image +assertAll: + - celExpr: "image.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/server' in image.metadata.finalizers" +commands: +- script: "! kubectl get flavor server-bdm-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/server/tests/server-bdm-dependency/02-delete-dependencies.yaml b/internal/controllers/server/tests/server-bdm-dependency/02-delete-dependencies.yaml new file mode 100644 index 000000000..6217bba41 --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/02-delete-dependencies.yaml @@ -0,0 +1,9 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # We should be able to delete the flavor (no deletion guard) + - command: kubectl delete flavor server-bdm-dependency + namespaced: true + # We expect the deletion to hang due to the finalizer, so use --wait=false + - command: kubectl delete image server-bdm-dependency --wait=false + namespaced: true diff --git a/internal/controllers/server/tests/server-bdm-dependency/03-assert.yaml b/internal/controllers/server/tests/server-bdm-dependency/03-assert.yaml new file mode 100644 index 000000000..4435482a3 --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/03-assert.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: "! kubectl get server server-bdm-dependency --namespace $NAMESPACE" + skipLogOutput: true +- script: "! kubectl get image server-bdm-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/server/tests/server-bdm-dependency/03-delete-server.yaml b/internal/controllers/server/tests/server-bdm-dependency/03-delete-server.yaml new file mode 100644 index 000000000..646a7bbe1 --- /dev/null +++ b/internal/controllers/server/tests/server-bdm-dependency/03-delete-server.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Server + name: server-bdm-dependency diff --git a/internal/controllers/server/tests/server-create-bdm-volume/00-assert.yaml b/internal/controllers/server/tests/server-create-bdm-volume/00-assert.yaml new file mode 100644 index 000000000..65d933fa8 --- /dev/null +++ b/internal/controllers/server/tests/server-create-bdm-volume/00-assert.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Server + name: server-create-bdm-volume + ref: server + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Volume + name: server-create-bdm-volume + ref: volume + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Port + name: server-create-bdm-volume + ref: port + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Network + name: server-create-bdm-volume + ref: network + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Subnet + name: server-create-bdm-volume + ref: subnet +assertAll: + - celExpr: "server.status.resource.hostID != ''" + - celExpr: "server.status.resource.availabilityZone != ''" + # Boot from existing volume: the volume should be attached + - celExpr: "server.status.resource.volumes.exists(v, v.id == volume.status.id)" + - celExpr: "port.status.resource.deviceID == server.status.id" + - celExpr: "port.status.resource.status == 'ACTIVE'" + - celExpr: "size(server.status.resource.interfaces) == 1" + - celExpr: "server.status.resource.interfaces[0].portID == port.status.id" + - celExpr: "server.status.resource.interfaces[0].netID == network.status.id" + - celExpr: "server.status.resource.interfaces[0].macAddr != ''" + - celExpr: "size(server.status.resource.interfaces[0].fixedIPs) >= 1" + - celExpr: "server.status.resource.interfaces[0].fixedIPs[0].subnetID == subnet.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-create-bdm-volume +status: + resource: + status: ACTIVE diff --git a/internal/controllers/server/tests/server-create-bdm-volume/00-create-resource.yaml b/internal/controllers/server/tests/server-create-bdm-volume/00-create-resource.yaml new file mode 100644 index 000000000..17c9f4c95 --- /dev/null +++ b/internal/controllers/server/tests/server-create-bdm-volume/00-create-resource.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Port +metadata: + name: server-create-bdm-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-create-bdm-volume + addresses: + - subnetRef: server-create-bdm-volume +--- +# Create a bootable volume from image +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: server-create-bdm-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + size: 1 + imageRef: server-create-bdm-volume +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-create-bdm-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + flavorRef: server-create-bdm-volume + ports: + - portRef: server-create-bdm-volume + blockDevices: + - sourceType: volume + volumeRef: server-create-bdm-volume + bootIndex: 0 diff --git a/internal/controllers/server/tests/server-create-bdm-volume/00-prerequisites.yaml b/internal/controllers/server/tests/server-create-bdm-volume/00-prerequisites.yaml new file mode 100644 index 000000000..964a80fb2 --- /dev/null +++ b/internal/controllers/server/tests/server-create-bdm-volume/00-prerequisites.yaml @@ -0,0 +1,50 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - script: | + export E2E_KUTTL_CURRENT_TEST=server-create-bdm-volume + cat ../templates/create-flavor.tmpl | envsubst | kubectl -n ${NAMESPACE} apply -f - +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: server-create-bdm-volume +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + content: + diskFormat: qcow2 + download: + url: https://github.com/k-orc/openstack-resource-controller/raw/2ddc1857f5e22d2f0df6f5ee033353e4fd907121/internal/controllers/image/testdata/cirros-0.6.3-x86_64-disk.img + visibility: public +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Network +metadata: + name: server-create-bdm-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: server-create-bdm-volume +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Subnet +metadata: + name: server-create-bdm-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-create-bdm-volume + ipVersion: 4 + cidr: 192.168.200.0/24 diff --git a/internal/controllers/server/tests/server-create-bdm/00-assert.yaml b/internal/controllers/server/tests/server-create-bdm/00-assert.yaml new file mode 100644 index 000000000..6586dfbb9 --- /dev/null +++ b/internal/controllers/server/tests/server-create-bdm/00-assert.yaml @@ -0,0 +1,41 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Server + name: server-create-bdm + ref: server + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Port + name: server-create-bdm + ref: port + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Network + name: server-create-bdm + ref: network + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Subnet + name: server-create-bdm + ref: subnet +assertAll: + - celExpr: "server.status.resource.hostID != ''" + - celExpr: "server.status.resource.availabilityZone != ''" + # Boot from volume: server should have at least one volume attached + - celExpr: "size(server.status.resource.volumes) >= 1" + - celExpr: "port.status.resource.deviceID == server.status.id" + - celExpr: "port.status.resource.status == 'ACTIVE'" + - celExpr: "size(server.status.resource.interfaces) == 1" + - celExpr: "server.status.resource.interfaces[0].portID == port.status.id" + - celExpr: "server.status.resource.interfaces[0].netID == network.status.id" + - celExpr: "server.status.resource.interfaces[0].macAddr != ''" + - celExpr: "size(server.status.resource.interfaces[0].fixedIPs) >= 1" + - celExpr: "server.status.resource.interfaces[0].fixedIPs[0].subnetID == subnet.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-create-bdm +status: + resource: + status: ACTIVE diff --git a/internal/controllers/server/tests/server-create-bdm/00-create-resource.yaml b/internal/controllers/server/tests/server-create-bdm/00-create-resource.yaml new file mode 100644 index 000000000..d260ca006 --- /dev/null +++ b/internal/controllers/server/tests/server-create-bdm/00-create-resource.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Port +metadata: + name: server-create-bdm +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-create-bdm + addresses: + - subnetRef: server-create-bdm +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-create-bdm +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + flavorRef: server-create-bdm + ports: + - portRef: server-create-bdm + blockDevices: + - sourceType: image + imageRef: server-create-bdm + bootIndex: 0 + volumeSizeGiB: 1 + destinationType: volume + deleteOnTermination: true \ No newline at end of file diff --git a/internal/controllers/server/tests/server-create-bdm/00-prerequisites.yaml b/internal/controllers/server/tests/server-create-bdm/00-prerequisites.yaml new file mode 100644 index 000000000..6e597192a --- /dev/null +++ b/internal/controllers/server/tests/server-create-bdm/00-prerequisites.yaml @@ -0,0 +1,50 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - script: | + export E2E_KUTTL_CURRENT_TEST=server-create-bdm + cat ../templates/create-flavor.tmpl | envsubst | kubectl -n ${NAMESPACE} apply -f - +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: server-create-bdm +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + content: + diskFormat: qcow2 + download: + url: https://github.com/k-orc/openstack-resource-controller/raw/2ddc1857f5e22d2f0df6f5ee033353e4fd907121/internal/controllers/image/testdata/cirros-0.6.3-x86_64-disk.img + visibility: public +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Network +metadata: + name: server-create-bdm +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: server-create-bdm +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Subnet +metadata: + name: server-create-bdm +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-create-bdm + ipVersion: 4 + cidr: 192.168.200.0/24