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/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
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
|