diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go index 7fefa5f38..4d977a732 100644 --- a/api/v1alpha1/server_types.go +++ b/api/v1alpha1/server_types.go @@ -60,6 +60,20 @@ type ServerPortSpec struct { PortRef *KubernetesNameRef `json:"portRef,omitempty"` } +// ServerBootVolumeSpec defines the boot volume for boot-from-volume server creation. +// When specified, the server boots from this volume instead of an image. +type ServerBootVolumeSpec struct { + // volumeRef is a reference to a Volume object. The volume must be + // bootable (created from an image) and available before server creation. + // +required + VolumeRef KubernetesNameRef `json:"volumeRef,omitempty"` + + // tag is the device tag applied to the volume. + // +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 +136,8 @@ type ServerInterfaceStatus struct { } // ServerResourceSpec contains the desired state of a server +// +kubebuilder:validation:XValidation:rule="has(self.imageRef) || has(self.bootVolume)",message="either imageRef or bootVolume must be specified" +// +kubebuilder:validation:XValidation:rule="!(has(self.imageRef) && has(self.bootVolume))",message="imageRef and bootVolume are mutually exclusive" 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,16 +145,23 @@ 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 field is required unless bootVolume is specified for boot-from-volume. + // +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 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="flavorRef is immutable" FlavorRef KubernetesNameRef `json:"flavorRef,omitempty"` + // bootVolume specifies a volume to boot from instead of an image. + // When specified, imageRef must be omitted. The volume must be + // bootable (created from an image using imageRef in the Volume spec). + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="bootVolume is immutable" + BootVolume *ServerBootVolumeSpec `json:"bootVolume,omitempty"` + // 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. @@ -158,12 +181,6 @@ type ServerResourceSpec struct { // +optional Volumes []ServerVolumeSpec `json:"volumes,omitempty"` - // serverGroupRef is a reference to a ServerGroup object. The server - // will be created in the server group. - // +optional - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="serverGroupRef is immutable" - ServerGroupRef *KubernetesNameRef `json:"serverGroupRef,omitempty"` - // availabilityZone is the availability zone in which to create the server. // +kubebuilder:validation:MaxLength=255 // +optional @@ -181,6 +198,90 @@ type ServerResourceSpec struct { // +listType=set // +optional Tags []ServerTag `json:"tags,omitempty"` + + // metadata is a list of metadata key-value pairs which will be set on the server. + // +kubebuilder:validation:MaxItems:=128 + // +listType=atomic + // +optional + Metadata []ServerMetadata `json:"metadata,omitempty"` + + // configDrive specifies whether to attach a config drive to the server. + // When true, configuration data will be available via a special drive + // instead of the metadata service. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="configDrive is immutable" + ConfigDrive *bool `json:"configDrive,omitempty"` + + // schedulerHints provides hints to the Nova scheduler for server placement. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="schedulerHints is immutable" + SchedulerHints *ServerSchedulerHints `json:"schedulerHints,omitempty"` +} + +// ServerSchedulerHints provides hints to the Nova scheduler for server placement. +type ServerSchedulerHints struct { + // serverGroupRef is a reference to a ServerGroup object. The server will be + // scheduled on a host in the specified server group. + // +optional + ServerGroupRef *KubernetesNameRef `json:"serverGroupRef,omitempty"` + + // differentHostServerRefs is a list of references to Server objects. + // The server will be scheduled on a different host than all specified servers. + // +listType=set + // +kubebuilder:validation:MaxItems:=64 + // +optional + DifferentHostServerRefs []KubernetesNameRef `json:"differentHostServerRefs,omitempty"` + + // sameHostServerRefs is a list of references to Server objects. + // The server will be scheduled on the same host as all specified servers. + // +listType=set + // +kubebuilder:validation:MaxItems:=64 + // +optional + SameHostServerRefs []KubernetesNameRef `json:"sameHostServerRefs,omitempty"` + + // query is a conditional statement that results in compute nodes + // able to host the server. + // +kubebuilder:validation:MaxLength:=1024 + // +optional + Query *string `json:"query,omitempty"` + + // targetCell is a cell name where the server will be placed. + // +kubebuilder:validation:MaxLength:=255 + // +optional + TargetCell *string `json:"targetCell,omitempty"` + + // differentCell is a list of cell names where the server should not + // be placed. + // +listType=set + // +kubebuilder:validation:MaxItems:=64 + // +kubebuilder:validation:items:MaxLength=1024 + // +optional + DifferentCell []string `json:"differentCell,omitempty"` + + // buildNearHostIP specifies a subnet of compute nodes to host the server. + // +kubebuilder:validation:MaxLength:=255 + // +optional + BuildNearHostIP *string `json:"buildNearHostIP,omitempty"` + + // additionalProperties is a map of arbitrary key/value pairs that are + // not validated by Nova. + // +optional + AdditionalProperties map[string]string `json:"additionalProperties,omitempty"` +} + +// ServerMetadata represents a key-value pair for server metadata. +type ServerMetadata struct { + // key is the metadata key. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +required + Key string `json:"key,omitempty"` + + // value is the metadata value. + // +kubebuilder:validation:MaxLength:=255 + // +kubebuilder:validation:MinLength:=1 + // +required + Value string `json:"value,omitempty"` } // +kubebuilder:validation:MinProperties:=1 @@ -261,4 +362,27 @@ type ServerResourceStatus struct { // +listType=atomic // +optional Tags []string `json:"tags,omitempty"` + + // metadata is the list of metadata key-value pairs on the resource. + // +kubebuilder:validation:MaxItems:=128 + // +listType=atomic + // +optional + Metadata []ServerMetadataStatus `json:"metadata,omitempty"` + + // configDrive indicates whether the server was booted with a config drive. + // +optional + ConfigDrive bool `json:"configDrive,omitempty"` +} + +// ServerMetadataStatus represents a key-value pair for server metadata in status. +type ServerMetadataStatus struct { + // key is the metadata key. + // +kubebuilder:validation:MaxLength:=255 + // +optional + Key string `json:"key,omitempty"` + + // value is the metadata value. + // +kubebuilder:validation:MaxLength:=255 + // +optional + Value string `json:"value,omitempty"` } diff --git a/api/v1alpha1/volume_types.go b/api/v1alpha1/volume_types.go index f50f83daa..49e2f3d06 100644 --- a/api/v1alpha1/volume_types.go +++ b/api/v1alpha1/volume_types.go @@ -56,6 +56,13 @@ type VolumeResourceSpec struct { // +listType=atomic // +optional Metadata []VolumeMetadata `json:"metadata,omitempty"` + + // imageRef is a reference to an ORC Image. If specified, creates a + // bootable volume from this image. The volume size must be >= the + // image's min_disk requirement. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="imageRef is immutable" + ImageRef *KubernetesNameRef `json:"imageRef,omitempty"` } // VolumeFilter defines an existing resource by its properties @@ -176,6 +183,11 @@ type VolumeResourceStatus struct { // +optional Bootable *bool `json:"bootable,omitempty"` + // imageID is the ID of the image this volume was created from, if any. + // +kubebuilder:validation:MaxLength=1024 + // +optional + ImageID string `json:"imageID,omitempty"` + // encrypted denotes if the volume is encrypted. // +optional Encrypted *bool `json:"encrypted,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 093e63451..01487d620 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3834,6 +3834,26 @@ 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 *ServerBootVolumeSpec) DeepCopyInto(out *ServerBootVolumeSpec) { + *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 ServerBootVolumeSpec. +func (in *ServerBootVolumeSpec) DeepCopy() *ServerBootVolumeSpec { + if in == nil { + return nil + } + out := new(ServerBootVolumeSpec) + 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 @@ -4194,6 +4214,36 @@ func (in *ServerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMetadata) DeepCopyInto(out *ServerMetadata) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMetadata. +func (in *ServerMetadata) DeepCopy() *ServerMetadata { + if in == nil { + return nil + } + out := new(ServerMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerMetadataStatus) DeepCopyInto(out *ServerMetadataStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerMetadataStatus. +func (in *ServerMetadataStatus) DeepCopy() *ServerMetadataStatus { + if in == nil { + return nil + } + out := new(ServerMetadataStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerPortSpec) DeepCopyInto(out *ServerPortSpec) { *out = *in @@ -4222,6 +4272,16 @@ 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.BootVolume != nil { + in, out := &in.BootVolume, &out.BootVolume + *out = new(ServerBootVolumeSpec) + (*in).DeepCopyInto(*out) + } if in.UserData != nil { in, out := &in.UserData, &out.UserData *out = new(UserDataSpec) @@ -4241,11 +4301,6 @@ func (in *ServerResourceSpec) DeepCopyInto(out *ServerResourceSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.ServerGroupRef != nil { - in, out := &in.ServerGroupRef, &out.ServerGroupRef - *out = new(KubernetesNameRef) - **out = **in - } if in.KeypairRef != nil { in, out := &in.KeypairRef, &out.KeypairRef *out = new(KubernetesNameRef) @@ -4256,6 +4311,21 @@ func (in *ServerResourceSpec) DeepCopyInto(out *ServerResourceSpec) { *out = make([]ServerTag, len(*in)) copy(*out, *in) } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make([]ServerMetadata, len(*in)) + copy(*out, *in) + } + if in.ConfigDrive != nil { + in, out := &in.ConfigDrive, &out.ConfigDrive + *out = new(bool) + **out = **in + } + if in.SchedulerHints != nil { + in, out := &in.SchedulerHints, &out.SchedulerHints + *out = new(ServerSchedulerHints) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerResourceSpec. @@ -4293,6 +4363,11 @@ func (in *ServerResourceStatus) DeepCopyInto(out *ServerResourceStatus) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make([]ServerMetadataStatus, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerResourceStatus. @@ -4305,6 +4380,63 @@ func (in *ServerResourceStatus) DeepCopy() *ServerResourceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerSchedulerHints) DeepCopyInto(out *ServerSchedulerHints) { + *out = *in + if in.ServerGroupRef != nil { + in, out := &in.ServerGroupRef, &out.ServerGroupRef + *out = new(KubernetesNameRef) + **out = **in + } + if in.DifferentHostServerRefs != nil { + in, out := &in.DifferentHostServerRefs, &out.DifferentHostServerRefs + *out = make([]KubernetesNameRef, len(*in)) + copy(*out, *in) + } + if in.SameHostServerRefs != nil { + in, out := &in.SameHostServerRefs, &out.SameHostServerRefs + *out = make([]KubernetesNameRef, len(*in)) + copy(*out, *in) + } + if in.Query != nil { + in, out := &in.Query, &out.Query + *out = new(string) + **out = **in + } + if in.TargetCell != nil { + in, out := &in.TargetCell, &out.TargetCell + *out = new(string) + **out = **in + } + if in.DifferentCell != nil { + in, out := &in.DifferentCell, &out.DifferentCell + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.BuildNearHostIP != nil { + in, out := &in.BuildNearHostIP, &out.BuildNearHostIP + *out = new(string) + **out = **in + } + if in.AdditionalProperties != nil { + in, out := &in.AdditionalProperties, &out.AdditionalProperties + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSchedulerHints. +func (in *ServerSchedulerHints) DeepCopy() *ServerSchedulerHints { + if in == nil { + return nil + } + out := new(ServerSchedulerHints) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { *out = *in @@ -5175,6 +5307,11 @@ func (in *VolumeResourceSpec) DeepCopyInto(out *VolumeResourceSpec) { *out = make([]VolumeMetadata, len(*in)) copy(*out, *in) } + if in.ImageRef != nil { + in, out := &in.ImageRef, &out.ImageRef + *out = new(KubernetesNameRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeResourceSpec. diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 8eab33c2d..bf2c14a45 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -160,6 +160,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.ServerBootVolumeSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ServerBootVolumeSpec(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), @@ -175,9 +176,12 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerInterfaceFixedIP": schema_openstack_resource_controller_v2_api_v1alpha1_ServerInterfaceFixedIP(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerInterfaceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ServerInterfaceStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerList": schema_openstack_resource_controller_v2_api_v1alpha1_ServerList(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerMetadata": schema_openstack_resource_controller_v2_api_v1alpha1_ServerMetadata(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerMetadataStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ServerMetadataStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerPortSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ServerPortSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerSchedulerHints": schema_openstack_resource_controller_v2_api_v1alpha1_ServerSchedulerHints(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ServerSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ServerStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerVolumeSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ServerVolumeSpec(ref), @@ -7434,6 +7438,34 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_Server(ref common.Refe } } +func schema_openstack_resource_controller_v2_api_v1alpha1_ServerBootVolumeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ServerBootVolumeSpec defines the boot volume for boot-from-volume server creation. When specified, the server boots from this volume instead of an image.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "volumeRef": { + SchemaProps: spec.SchemaProps{ + Description: "volumeRef is a reference to a Volume object. The volume must be bootable (created from an image) and available before server creation.", + Type: []string{"string"}, + Format: "", + }, + }, + "tag": { + SchemaProps: spec.SchemaProps{ + Description: "tag is the device tag applied to the volume.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"volumeRef"}, + }, + }, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_ServerFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -8079,6 +8111,61 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerList(ref common. } } +func schema_openstack_resource_controller_v2_api_v1alpha1_ServerMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ServerMetadata represents a key-value pair for server metadata.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the metadata key.", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "value is the metadata value.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"key", "value"}, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_ServerMetadataStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ServerMetadataStatus represents a key-value pair for server metadata in status.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the metadata key.", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "value is the metadata value.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_ServerPortSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -8114,7 +8201,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 field is required unless bootVolume is specified for boot-from-volume.", Type: []string{"string"}, Format: "", }, @@ -8126,6 +8213,12 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref Format: "", }, }, + "bootVolume": { + SchemaProps: spec.SchemaProps{ + Description: "bootVolume specifies a volume to boot from instead of an image. When specified, imageRef must be omitted. The volume must be bootable (created from an image using imageRef in the Volume spec).", + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerBootVolumeSpec"), + }, + }, "userData": { SchemaProps: spec.SchemaProps{ Description: "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.", @@ -8170,13 +8263,6 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref }, }, }, - "serverGroupRef": { - SchemaProps: spec.SchemaProps{ - Description: "serverGroupRef is a reference to a ServerGroup object. The server will be created in the server group.", - Type: []string{"string"}, - Format: "", - }, - }, "availabilityZone": { SchemaProps: spec.SchemaProps{ Description: "availabilityZone is the availability zone in which to create the server.", @@ -8211,12 +8297,44 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceSpec(ref }, }, }, + "metadata": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "metadata is a list of metadata key-value pairs which will be set on the server.", + 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.ServerMetadata"), + }, + }, + }, + }, + }, + "configDrive": { + SchemaProps: spec.SchemaProps{ + Description: "configDrive specifies whether to attach a config drive to the server. When true, configuration data will be available via a special drive instead of the metadata service.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "schedulerHints": { + SchemaProps: spec.SchemaProps{ + Description: "schedulerHints provides hints to the Nova scheduler for server placement.", + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerSchedulerHints"), + }, + }, }, - Required: []string{"imageRef", "flavorRef", "ports"}, + Required: []string{"flavorRef", "ports"}, }, }, Dependencies: []string{ - "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.ServerBootVolumeSpec", "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.ServerSchedulerHints", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerVolumeSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.UserDataSpec"}, } } @@ -8340,11 +8458,154 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_ServerResourceStatus(r }, }, }, + "metadata": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "metadata is the list of metadata key-value pairs on the resource.", + 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.ServerMetadataStatus"), + }, + }, + }, + }, + }, + "configDrive": { + SchemaProps: spec.SchemaProps{ + Description: "configDrive indicates whether the server was booted with a config drive.", + Type: []string{"boolean"}, + Format: "", + }, + }, }, }, }, Dependencies: []string{ - "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerInterfaceStatus", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerVolumeStatus"}, + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerInterfaceStatus", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerMetadataStatus", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ServerVolumeStatus"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_ServerSchedulerHints(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ServerSchedulerHints provides hints to the Nova scheduler for server placement.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "serverGroupRef": { + SchemaProps: spec.SchemaProps{ + Description: "serverGroupRef is a reference to a ServerGroup object. The server will be scheduled on a host in the specified server group.", + Type: []string{"string"}, + Format: "", + }, + }, + "differentHostServerRefs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "differentHostServerRefs is a list of references to Server objects. The server will be scheduled on a different host than all specified servers.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "sameHostServerRefs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "sameHostServerRefs is a list of references to Server objects. The server will be scheduled on the same host as all specified servers.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Description: "query is a conditional statement that results in compute nodes able to host the server.", + Type: []string{"string"}, + Format: "", + }, + }, + "targetCell": { + SchemaProps: spec.SchemaProps{ + Description: "targetCell is a cell name where the server will be placed.", + Type: []string{"string"}, + Format: "", + }, + }, + "differentCell": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "differentCell is a list of cell names where the server should not be placed.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "buildNearHostIP": { + SchemaProps: spec.SchemaProps{ + Description: "buildNearHostIP specifies a subnet of compute nodes to host the server.", + Type: []string{"string"}, + Format: "", + }, + }, + "additionalProperties": { + SchemaProps: spec.SchemaProps{ + Description: "additionalProperties is a map of arbitrary key/value pairs that are not validated by Nova.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, } } @@ -9953,6 +10214,13 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_VolumeResourceSpec(ref }, }, }, + "imageRef": { + SchemaProps: spec.SchemaProps{ + Description: "imageRef is a reference to an ORC Image. If specified, creates a bootable volume from this image. The volume size must be >= the image's min_disk requirement.", + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"size"}, }, @@ -10084,6 +10352,13 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_VolumeResourceStatus(r Format: "", }, }, + "imageID": { + SchemaProps: spec.SchemaProps{ + Description: "imageID is the ID of the image this volume was created from, if any.", + Type: []string{"string"}, + Format: "", + }, + }, "encrypted": { SchemaProps: spec.SchemaProps{ Description: "encrypted denotes if the volume is encrypted.", diff --git a/config/crd/bases/openstack.k-orc.cloud_servers.yaml b/config/crd/bases/openstack.k-orc.cloud_servers.yaml index c9feaab67..28ade6ac1 100644 --- a/config/crd/bases/openstack.k-orc.cloud_servers.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_servers.yaml @@ -202,6 +202,38 @@ spec: x-kubernetes-validations: - message: availabilityZone is immutable rule: self == oldSelf + bootVolume: + description: |- + bootVolume specifies a volume to boot from instead of an image. + When specified, imageRef must be omitted. The volume must be + bootable (created from an image using imageRef in the Volume spec). + properties: + tag: + description: tag is the device tag applied to the volume. + maxLength: 255 + type: string + volumeRef: + description: |- + volumeRef is a reference to a Volume object. The volume must be + bootable (created from an image) and available before server creation. + maxLength: 253 + minLength: 1 + type: string + required: + - volumeRef + type: object + x-kubernetes-validations: + - message: bootVolume is immutable + rule: self == oldSelf + configDrive: + description: |- + configDrive specifies whether to attach a config drive to the server. + When true, configuration data will be available via a special drive + instead of the metadata service. + type: boolean + x-kubernetes-validations: + - message: configDrive is immutable + rule: self == oldSelf flavorRef: description: flavorRef references the flavor to use for the server instance. @@ -214,7 +246,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 field is required unless bootVolume is specified for boot-from-volume. maxLength: 253 minLength: 1 type: string @@ -231,6 +263,30 @@ spec: x-kubernetes-validations: - message: keypairRef is immutable rule: self == oldSelf + metadata: + description: metadata is a list of metadata key-value pairs which + will be set on the server. + items: + description: ServerMetadata represents a key-value pair for + server metadata. + properties: + key: + description: key is the metadata key. + maxLength: 255 + minLength: 1 + type: string + value: + description: value is the metadata value. + maxLength: 255 + minLength: 1 + type: string + required: + - key + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-type: atomic name: description: |- name will be the name of the created resource. If not specified, the @@ -257,15 +313,75 @@ spec: maxItems: 64 type: array x-kubernetes-list-type: atomic - serverGroupRef: - description: |- - serverGroupRef is a reference to a ServerGroup object. The server - will be created in the server group. - maxLength: 253 - minLength: 1 - type: string + schedulerHints: + description: schedulerHints provides hints to the Nova scheduler + for server placement. + properties: + additionalProperties: + additionalProperties: + type: string + description: |- + additionalProperties is a map of arbitrary key/value pairs that are + not validated by Nova. + type: object + buildNearHostIP: + description: buildNearHostIP specifies a subnet of compute + nodes to host the server. + maxLength: 255 + type: string + differentCell: + description: |- + differentCell is a list of cell names where the server should not + be placed. + items: + maxLength: 1024 + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + differentHostServerRefs: + description: |- + differentHostServerRefs is a list of references to Server objects. + The server will be scheduled on a different host than all specified servers. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + query: + description: |- + query is a conditional statement that results in compute nodes + able to host the server. + maxLength: 1024 + type: string + sameHostServerRefs: + description: |- + sameHostServerRefs is a list of references to Server objects. + The server will be scheduled on the same host as all specified servers. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + serverGroupRef: + description: |- + serverGroupRef is a reference to a ServerGroup object. The server will be + scheduled on a host in the specified server group. + maxLength: 253 + minLength: 1 + type: string + targetCell: + description: targetCell is a cell name where the server will + be placed. + maxLength: 255 + type: string + type: object x-kubernetes-validations: - - message: serverGroupRef is immutable + - message: schedulerHints is immutable rule: self == oldSelf tags: description: tags is a list of tags which will be applied to the @@ -321,9 +437,13 @@ spec: x-kubernetes-list-type: atomic required: - flavorRef - - imageRef - ports type: object + x-kubernetes-validations: + - message: either imageRef or bootVolume must be specified + rule: has(self.imageRef) || has(self.bootVolume) + - message: imageRef and bootVolume are mutually exclusive + rule: '!(has(self.imageRef) && has(self.bootVolume))' required: - cloudCredentialsRef type: object @@ -431,6 +551,10 @@ spec: server is located. maxLength: 1024 type: string + configDrive: + description: configDrive indicates whether the server was booted + with a config drive. + type: boolean hostID: description: hostID is the host where the server is located in the cloud. @@ -488,6 +612,25 @@ spec: maxItems: 64 type: array x-kubernetes-list-type: atomic + metadata: + description: metadata is the list of metadata key-value pairs + on the resource. + items: + description: ServerMetadataStatus represents a key-value pair + for server metadata in status. + properties: + key: + description: key is the metadata key. + maxLength: 255 + type: string + value: + description: value is the metadata value. + maxLength: 255 + type: string + type: object + maxItems: 128 + type: array + x-kubernetes-list-type: atomic name: description: name is the human-readable name of the resource. Might not be unique. diff --git a/config/crd/bases/openstack.k-orc.cloud_volumes.yaml b/config/crd/bases/openstack.k-orc.cloud_volumes.yaml index aca503047..eeaf10a8b 100644 --- a/config/crd/bases/openstack.k-orc.cloud_volumes.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_volumes.yaml @@ -173,6 +173,17 @@ spec: maxLength: 255 minLength: 1 type: string + imageRef: + description: |- + imageRef is a reference to an ORC Image. If specified, creates a + bootable volume from this image. The volume size must be >= the + image's min_disk requirement. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: imageRef is immutable + rule: self == oldSelf metadata: description: |- metadata key and value pairs to be associated with the volume. @@ -389,6 +400,11 @@ spec: description: host is the identifier of the host holding the volume. maxLength: 1024 type: string + imageID: + description: imageID is the ID of the image this volume was created + from, if any. + maxLength: 1024 + type: string metadata: description: metadata key and value pairs to be associated with the volume. diff --git a/config/samples/openstack_v1alpha1_server.yaml b/config/samples/openstack_v1alpha1_server.yaml index 29691f536..0ee8c24ff 100644 --- a/config/samples/openstack_v1alpha1_server.yaml +++ b/config/samples/openstack_v1alpha1_server.yaml @@ -14,9 +14,16 @@ spec: - portRef: server-sample volumes: - volumeRef: server-sample - serverGroupRef: server-sample keypairRef: server-sample + schedulerHints: + serverGroupRef: server-sample availabilityZone: nova tags: - tag1 - tag2 + metadata: + - key: environment + value: development + - key: owner + value: sample + configDrive: true diff --git a/config/samples/openstack_v1alpha1_server_boot_from_volume.yaml b/config/samples/openstack_v1alpha1_server_boot_from_volume.yaml new file mode 100644 index 000000000..a63d01abc --- /dev/null +++ b/config/samples/openstack_v1alpha1_server_boot_from_volume.yaml @@ -0,0 +1,25 @@ +# Example of creating a server that boots from a Cinder volume instead of an image. +# This is the boot-from-volume (BFV) pattern. +# +# Prerequisites: +# - A bootable volume created from an image (see openstack_v1alpha1_volume_bootable.yaml) +# - Network, subnet, and port resources +# - A flavor +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-boot-from-volume-sample +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + # Note: No imageRef - booting from volume instead + bootVolume: + volumeRef: bootable-volume-sample + flavorRef: server-sample + ports: + - portRef: server-sample + availabilityZone: nova diff --git a/config/samples/openstack_v1alpha1_volume_bootable.yaml b/config/samples/openstack_v1alpha1_volume_bootable.yaml new file mode 100644 index 000000000..a1f80ba0c --- /dev/null +++ b/config/samples/openstack_v1alpha1_volume_bootable.yaml @@ -0,0 +1,29 @@ +--- +# Example of creating a bootable volume from an image. +# The volume can then be used as a boot device for a server. +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: ubuntu-2404 +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + import: + filter: + name: ubuntu-24.04-server +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: bootable-volume-sample +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Bootable volume created from Ubuntu image + size: 50 + imageRef: ubuntu-2404 + volumeTypeRef: fast-ssd diff --git a/examples/bases/boot-from-volume/kustomization.yaml b/examples/bases/boot-from-volume/kustomization.yaml new file mode 100644 index 000000000..ce2cb1adc --- /dev/null +++ b/examples/bases/boot-from-volume/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- volume.yaml +- server.yaml diff --git a/examples/bases/boot-from-volume/server.yaml b/examples/bases/boot-from-volume/server.yaml new file mode 100644 index 000000000..129203407 --- /dev/null +++ b/examples/bases/boot-from-volume/server.yaml @@ -0,0 +1,18 @@ +--- +# Server that boots from a volume instead of an image +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: cloud-config + managementPolicy: managed + resource: + # No imageRef - booting from volume + bootVolume: + volumeRef: boot-volume + flavorRef: flavor + ports: + - portRef: port diff --git a/examples/bases/boot-from-volume/volume.yaml b/examples/bases/boot-from-volume/volume.yaml new file mode 100644 index 000000000..007353870 --- /dev/null +++ b/examples/bases/boot-from-volume/volume.yaml @@ -0,0 +1,14 @@ +--- +# Bootable volume created from the cirros image +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: boot-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: cloud-config + managementPolicy: managed + resource: + size: 1 + imageRef: cirros diff --git a/internal/controllers/server/actuator.go b/internal/controllers/server/actuator.go index 6a2118695..9de206047 100644 --- a/internal/controllers/server/actuator.go +++ b/internal/controllers/server/actuator.go @@ -178,13 +178,76 @@ func (actuator serverActuator) getFlavorHelper(ctx context.Context, obj *orcv1al }, &orcv1alpha1.Flavor{}) } -func (actuator serverActuator) getServerGroupHelper(ctx context.Context, obj *orcv1alpha1.Server, resource *orcv1alpha1.ServerResourceSpec) (*orcv1alpha1.ServerGroup, progress.ReconcileStatus) { - if resource.ServerGroupRef == nil { - return &orcv1alpha1.ServerGroup{}, progress.NewReconcileStatus() +func (actuator serverActuator) getSchedulerHintsHelper(ctx context.Context, obj *orcv1alpha1.Server, resource *orcv1alpha1.ServerResourceSpec) (servers.SchedulerHintOpts, progress.ReconcileStatus) { + hints := servers.SchedulerHintOpts{} + + if resource.SchedulerHints == nil { + return hints, progress.NewReconcileStatus() + } + + schedHints := resource.SchedulerHints + reconcileStatus := progress.NewReconcileStatus() + + // Resolve ServerGroupRef to server group ID + if schedHints.ServerGroupRef != nil { + sg, sgReconcileStatus := getDependencyHelper(ctx, actuator.k8sClient, obj, string(*schedHints.ServerGroupRef), "ServerGroup", func(sg *orcv1alpha1.ServerGroup) bool { + return orcv1alpha1.IsAvailable(sg) && sg.Status.ID != nil + }, &orcv1alpha1.ServerGroup{}) + reconcileStatus = reconcileStatus.WithReconcileStatus(sgReconcileStatus) + if sg.Status.ID != nil { + hints.Group = *sg.Status.ID + } + } + + // Resolve differentHostServerRefs to server IDs + if len(schedHints.DifferentHostServerRefs) > 0 { + differentHost := make([]string, 0, len(schedHints.DifferentHostServerRefs)) + for _, ref := range schedHints.DifferentHostServerRefs { + server, serverReconcileStatus := getDependencyHelper(ctx, actuator.k8sClient, obj, string(ref), "Server", func(s *orcv1alpha1.Server) bool { + return s.Status.ID != nil + }, &orcv1alpha1.Server{}) + reconcileStatus = reconcileStatus.WithReconcileStatus(serverReconcileStatus) + if server.Status.ID != nil { + differentHost = append(differentHost, *server.Status.ID) + } + } + hints.DifferentHost = differentHost + } + + // Resolve sameHostServerRefs to server IDs + if len(schedHints.SameHostServerRefs) > 0 { + sameHost := make([]string, 0, len(schedHints.SameHostServerRefs)) + for _, ref := range schedHints.SameHostServerRefs { + server, serverReconcileStatus := getDependencyHelper(ctx, actuator.k8sClient, obj, string(ref), "Server", func(s *orcv1alpha1.Server) bool { + return s.Status.ID != nil + }, &orcv1alpha1.Server{}) + reconcileStatus = reconcileStatus.WithReconcileStatus(serverReconcileStatus) + if server.Status.ID != nil { + sameHost = append(sameHost, *server.Status.ID) + } + } + hints.SameHost = sameHost + } + + if schedHints.Query != nil { + hints.Query = []any{*schedHints.Query} } - return getDependencyHelper(ctx, actuator.k8sClient, obj, string(*resource.ServerGroupRef), "ServerGroup", func(sg *orcv1alpha1.ServerGroup) bool { - return orcv1alpha1.IsAvailable(sg) && sg.Status.ID != nil - }, &orcv1alpha1.ServerGroup{}) + if schedHints.TargetCell != nil { + hints.TargetCell = *schedHints.TargetCell + } + hints.DifferentCell = schedHints.DifferentCell + if schedHints.BuildNearHostIP != nil { + hints.BuildNearHostIP = *schedHints.BuildNearHostIP + } + if schedHints.AdditionalProperties != nil { + additionalProps := make(map[string]any, len(schedHints.AdditionalProperties)) + for k, v := range schedHints.AdditionalProperties { + additionalProps[k] = v + } + hints.AdditionalProperties = additionalProps + } + + return hints, reconcileStatus } func (actuator serverActuator) getKeypairHelper(ctx context.Context, obj *orcv1alpha1.Server, resource *orcv1alpha1.ServerResourceSpec) (*orcv1alpha1.KeyPair, progress.ReconcileStatus) { @@ -223,15 +286,45 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp reconcileStatus := progress.NewReconcileStatus() - var image *orcv1alpha1.Image - { + // Determine if we're booting from volume or image + bootFromVolume := resource.BootVolume != nil + + var imageID string + if !bootFromVolume { + // Traditional boot from image dep, imageReconcileStatus := imageDependency.GetDependency( ctx, actuator.k8sClient, obj, func(image *orcv1alpha1.Image) bool { return orcv1alpha1.IsAvailable(image) && image.Status.ID != nil }, ) reconcileStatus = reconcileStatus.WithReconcileStatus(imageReconcileStatus) - image = dep + if dep != nil && dep.Status.ID != nil { + imageID = *dep.Status.ID + } + } + + // Resolve boot volume for boot-from-volume + var blockDevices []servers.BlockDevice + if bootFromVolume { + bootVolume, bvReconcileStatus := bootVolumeDependency.GetDependency( + ctx, actuator.k8sClient, obj, func(volume *orcv1alpha1.Volume) bool { + return orcv1alpha1.IsAvailable(volume) && volume.Status.ID != nil + }, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(bvReconcileStatus) + + if bootVolume != nil && bootVolume.Status.ID != nil { + bd := servers.BlockDevice{ + SourceType: servers.SourceVolume, + DestinationType: servers.DestinationVolume, + UUID: *bootVolume.Status.ID, + BootIndex: 0, // Always 0 for boot volume + } + if resource.BootVolume.Tag != nil { + bd.Tag = *resource.BootVolume.Tag + } + blockDevices = append(blockDevices, bd) + } } flavor, flavorReconcileStatus := actuator.getFlavorHelper(ctx, obj, resource) @@ -265,8 +358,8 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp } } - serverGroup, serverGroupReconcileStatus := actuator.getServerGroupHelper(ctx, obj, resource) - reconcileStatus = reconcileStatus.WithReconcileStatus(serverGroupReconcileStatus) + schedulerHints, schedulerHintsReconcileStatus := actuator.getSchedulerHintsHelper(ctx, obj, resource) + reconcileStatus = reconcileStatus.WithReconcileStatus(schedulerHintsReconcileStatus) keypair, keypairReconcileStatus := actuator.getKeypairHelper(ctx, obj, resource) reconcileStatus = reconcileStatus.WithReconcileStatus(keypairReconcileStatus) @@ -285,14 +378,22 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp // Sort tags before creation to simplify comparisons slices.Sort(tags) + metadata := make(map[string]string) + for _, m := range resource.Metadata { + metadata[m.Key] = m.Value + } + serverCreateOpts := servers.CreateOpts{ Name: getResourceName(obj), - ImageRef: *image.Status.ID, + ImageRef: imageID, // Empty string if boot-from-volume FlavorRef: *flavor.Status.ID, Networks: portList, UserData: userData, Tags: tags, + Metadata: metadata, AvailabilityZone: resource.AvailabilityZone, + ConfigDrive: resource.ConfigDrive, + BlockDevice: blockDevices, // Boot volume for BFV } /* keypairs.CreateOptsExt was merged into servers.CreateOpts in gopher cloud V3 @@ -306,10 +407,6 @@ func (actuator serverActuator) CreateResource(ctx context.Context, obj *orcv1alp } } - schedulerHints := servers.SchedulerHintOpts{ - Group: ptr.Deref(serverGroup.Status.ID, ""), - } - server, err := actuator.osClient.CreateServer(ctx, createOpts, schedulerHints) // We should require the spec to be updated before retrying a create which returned a non-retryable error @@ -343,6 +440,7 @@ func (actuator serverActuator) GetResourceReconcilers(ctx context.Context, orcOb actuator.checkStatus, actuator.updateResource, actuator.reconcileTags, + actuator.reconcileMetadata, actuator.reconcilePortAttachments, actuator.reconcileVolumeAttachments, }, nil @@ -429,6 +527,39 @@ func (actuator serverActuator) reconcileTags(ctx context.Context, obj orcObjectP return tags.ReconcileTags[orcObjectPT, osResourceT](obj.Spec.Resource.Tags, ptr.Deref(osResource.Tags, []string{}), tags.NewServerTagReplacer(actuator.osClient, osResource.ID))(ctx, obj, osResource) } +func (actuator serverActuator) reconcileMetadata(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { + log := ctrl.LoggerFrom(ctx) + resource := obj.Spec.Resource + if resource == nil { + return progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Update requested, but spec.resource is not set")) + } + + // Metadata cannot be set on a server that is still building + if osResource.Status == "" || osResource.Status == ServerStatusBuild { + return progress.NewReconcileStatus().WaitingOnOpenStack(progress.WaitingOnReady, serverActivePollingPeriod) + } + + // Build the desired metadata map from spec + desiredMetadata := make(map[string]string) + for _, m := range resource.Metadata { + desiredMetadata[m.Key] = m.Value + } + + // Compare with current metadata + if maps.Equal(desiredMetadata, osResource.Metadata) { + return nil + } + + log.V(logging.Verbose).Info("Updating server metadata") + _, err := actuator.osClient.ReplaceServerMetadata(ctx, osResource.ID, desiredMetadata) + if err != nil { + return progress.WrapError(err) + } + + return progress.NeedsRefresh() +} + func (actuator serverActuator) reconcilePortAttachments(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { log := ctrl.LoggerFrom(ctx) resource := obj.Spec.Resource diff --git a/internal/controllers/server/controller.go b/internal/controllers/server/controller.go index 95ac9f595..d5ca0f598 100644 --- a/internal/controllers/server/controller.go +++ b/internal/controllers/server/controller.go @@ -73,13 +73,31 @@ 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, + ) + + // bootVolumeDependency handles the boot volume specified in bootVolume for boot-from-volume. + // This volume is attached at server creation time as the root disk. + // deletion guard is in place because the server cannot boot without its root volume. + // OverrideDependencyName is used to avoid conflict with volumeDependency which also + // creates a Volume deletion guard for Server. + bootVolumeDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.ServerList, *orcv1alpha1.Volume]( + "spec.resource.bootVolume.volumeRef", + func(server *orcv1alpha1.Server) []string { + resource := server.Spec.Resource + if resource == nil || resource.BootVolume == nil { + return nil + } + return []string{string(resource.BootVolume.VolumeRef)} }, finalizer, externalObjectFieldOwner, + dependency.OverrideDependencyName("bootvolume"), ) portDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.ServerList, *orcv1alpha1.Port]( @@ -105,14 +123,14 @@ var ( // No deletion guard for server group, because server group can be safely deleted while // referenced by a server serverGroupDependency = dependency.NewDependency[*orcv1alpha1.ServerList, *orcv1alpha1.ServerGroup]( - "spec.resource.serverGroupRef", + "spec.resource.schedulerHints.serverGroupRef", func(server *orcv1alpha1.Server) []string { resource := server.Spec.Resource - if resource == nil || resource.ServerGroupRef == nil { + if resource == nil || resource.SchedulerHints == nil || resource.SchedulerHints.ServerGroupRef == nil { return nil } - return []string{string(*resource.ServerGroupRef)} + return []string{string(*resource.SchedulerHints.ServerGroupRef)} }, ) @@ -196,6 +214,10 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c if err != nil { return err } + bootVolumeWatchEventHandler, err := bootVolumeDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). @@ -215,6 +237,9 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c Watches(&orcv1alpha1.Volume{}, volumeWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Volume{})), ). + Watches(&orcv1alpha1.Volume{}, bootVolumeWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Volume{})), + ). Watches(&orcv1alpha1.KeyPair{}, keypairWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.KeyPair{})), ). @@ -234,6 +259,7 @@ func (c serverReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c serverGroupDependency.AddToManager(ctx, mgr), userDataDependency.AddToManager(ctx, mgr), volumeDependency.AddToManager(ctx, mgr), + bootVolumeDependency.AddToManager(ctx, mgr), keypairDependency.AddToManager(ctx, mgr), credentialsDependency.AddToManager(ctx, mgr), credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), diff --git a/internal/controllers/server/status.go b/internal/controllers/server/status.go index aa7c47ccf..39956c531 100644 --- a/internal/controllers/server/status.go +++ b/internal/controllers/server/status.go @@ -18,6 +18,8 @@ package server import ( "fmt" + "maps" + "slices" "github.com/go-logr/logr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -69,7 +71,8 @@ func (serverStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osRes WithHostID(osResource.HostID). WithAvailabilityZone(osResource.AvailabilityZone). WithServerGroups(ptr.Deref(osResource.ServerGroups, []string{})...). - WithTags(ptr.Deref(osResource.Tags, []string{})...) + WithTags(ptr.Deref(osResource.Tags, []string{})...). + WithConfigDrive(osResource.ConfigDrive) if imageID, ok := osResource.Image["id"]; ok { status.WithImageID(fmt.Sprintf("%s", imageID)) @@ -97,5 +100,12 @@ func (serverStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osRes status.WithInterfaces(interfaceStatus) } + // Sort metadata keys for deterministic output + for _, k := range slices.Sorted(maps.Keys(osResource.Metadata)) { + status.WithMetadata(orcapplyconfigv1alpha1.ServerMetadataStatus(). + WithKey(k). + WithValue(osResource.Metadata[k])) + } + statusApply.WithResource(status) } diff --git a/internal/controllers/server/tests/server-boot-from-volume/00-assert.yaml b/internal/controllers/server/tests/server-boot-from-volume/00-assert.yaml new file mode 100644 index 000000000..4fa211ad6 --- /dev/null +++ b/internal/controllers/server/tests/server-boot-from-volume/00-assert.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Server + name: server-boot-from-volume + ref: server + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Volume + name: server-boot-from-volume + ref: volume + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Port + name: server-boot-from-volume + ref: port + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Network + name: server-boot-from-volume + ref: network + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Subnet + name: server-boot-from-volume + ref: subnet +assertAll: + - celExpr: "server.status.resource.hostID != ''" + - celExpr: "server.status.resource.availabilityZone != ''" + # Verify the server booted from volume (imageID may be empty for BFV servers) + - 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: "server.status.resource.interfaces[0].portState != ''" + - celExpr: "size(server.status.resource.interfaces[0].fixedIPs) >= 1" + - celExpr: "server.status.resource.interfaces[0].fixedIPs[0].ipAddress != ''" + - celExpr: "server.status.resource.interfaces[0].fixedIPs[0].subnetID == subnet.status.id" + # Verify volume is bootable + - celExpr: "volume.status.resource.bootable == true" + # Verify volume is attached to the server + - celExpr: "size(volume.status.resource.attachments) == 1" + - celExpr: "volume.status.resource.attachments[0].serverID == server.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-boot-from-volume +status: + resource: + name: server-boot-from-volume + status: ACTIVE +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: server-boot-from-volume +status: + resource: + bootable: true + status: in-use diff --git a/internal/controllers/server/tests/server-boot-from-volume/00-create-resource.yaml b/internal/controllers/server/tests/server-boot-from-volume/00-create-resource.yaml new file mode 100644 index 000000000..e14a11c45 --- /dev/null +++ b/internal/controllers/server/tests/server-boot-from-volume/00-create-resource.yaml @@ -0,0 +1,30 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Port +metadata: + name: server-boot-from-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-boot-from-volume + addresses: + - subnetRef: server-boot-from-volume +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Server +metadata: + name: server-boot-from-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + # Note: No imageRef - booting from volume! + bootVolume: + volumeRef: server-boot-from-volume + flavorRef: server-boot-from-volume + ports: + - portRef: server-boot-from-volume diff --git a/internal/controllers/server/tests/server-boot-from-volume/00-prerequisites.yaml b/internal/controllers/server/tests/server-boot-from-volume/00-prerequisites.yaml new file mode 100644 index 000000000..a75dc4d64 --- /dev/null +++ b/internal/controllers/server/tests/server-boot-from-volume/00-prerequisites.yaml @@ -0,0 +1,63 @@ +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-boot-from-volume + cat ../templates/create-flavor.tmpl | envsubst | kubectl -n ${NAMESPACE} apply -f - +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: server-boot-from-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-boot-from-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: server-boot-from-volume +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Subnet +metadata: + name: server-boot-from-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + networkRef: server-boot-from-volume + ipVersion: 4 + cidr: 192.168.201.0/24 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: server-boot-from-volume +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + size: 1 + imageRef: server-boot-from-volume diff --git a/internal/controllers/server/tests/server-boot-from-volume/README.md b/internal/controllers/server/tests/server-boot-from-volume/README.md new file mode 100644 index 000000000..0a26653ad --- /dev/null +++ b/internal/controllers/server/tests/server-boot-from-volume/README.md @@ -0,0 +1,14 @@ +# Boot from Volume Test + +This test creates a server that boots from a Cinder volume instead of an +image. This is the boot-from-volume (BFV) pattern where: + +1. An image is created +2. A bootable volume is created from that image +3. A server is created booting from the volume (no imageRef) + +The test verifies: +- Server reaches ACTIVE state +- Volume is marked as bootable +- Volume is attached to the server +- Port is attached to the server diff --git a/internal/controllers/server/tests/server-create-full/00-assert.yaml b/internal/controllers/server/tests/server-create-full/00-assert.yaml index 5f351d6d1..68c65c73b 100644 --- a/internal/controllers/server/tests/server-create-full/00-assert.yaml +++ b/internal/controllers/server/tests/server-create-full/00-assert.yaml @@ -66,3 +66,9 @@ status: tags: - tag1 - tag2 + metadata: + - key: environment + value: test + - key: owner + value: kuttl + configDrive: true diff --git a/internal/controllers/server/tests/server-create-full/00-create-resource.yaml b/internal/controllers/server/tests/server-create-full/00-create-resource.yaml index 006b18145..28b64f532 100644 --- a/internal/controllers/server/tests/server-create-full/00-create-resource.yaml +++ b/internal/controllers/server/tests/server-create-full/00-create-resource.yaml @@ -40,11 +40,18 @@ spec: flavorRef: server-create-full ports: - portRef: server-create-full - serverGroupRef: server-create-full keypairRef: server-create-full + schedulerHints: + serverGroupRef: server-create-full volumes: - volumeRef: server-create-full availabilityZone: nova tags: - tag1 - tag2 + metadata: + - key: environment + value: test + - key: owner + value: kuttl + configDrive: true diff --git a/internal/controllers/server/tests/server-dependency/00-create-everything-but-flavor.yaml b/internal/controllers/server/tests/server-dependency/00-create-everything-but-flavor.yaml index 101976fcf..ae93fc454 100644 --- a/internal/controllers/server/tests/server-dependency/00-create-everything-but-flavor.yaml +++ b/internal/controllers/server/tests/server-dependency/00-create-everything-but-flavor.yaml @@ -87,6 +87,7 @@ spec: flavorRef: server-dependency ports: - portRef: server-dependency - serverGroupRef: server-dependency + schedulerHints: + serverGroupRef: server-dependency userData: secretRef: server-dependency \ No newline at end of file diff --git a/internal/controllers/server/tests/server-dependency/01-create-everything-but-image.yaml b/internal/controllers/server/tests/server-dependency/01-create-everything-but-image.yaml index 5757e4eea..a669f622f 100644 --- a/internal/controllers/server/tests/server-dependency/01-create-everything-but-image.yaml +++ b/internal/controllers/server/tests/server-dependency/01-create-everything-but-image.yaml @@ -27,6 +27,7 @@ spec: flavorRef: server-dependency ports: - portRef: server-dependency - serverGroupRef: server-dependency + schedulerHints: + serverGroupRef: server-dependency userData: secretRef: server-dependency diff --git a/internal/controllers/server/tests/server-dependency/02-create-everything-but-port.yaml b/internal/controllers/server/tests/server-dependency/02-create-everything-but-port.yaml index 4dd1e19b0..45f5348cc 100644 --- a/internal/controllers/server/tests/server-dependency/02-create-everything-but-port.yaml +++ b/internal/controllers/server/tests/server-dependency/02-create-everything-but-port.yaml @@ -38,6 +38,7 @@ spec: flavorRef: server-dependency ports: - portRef: server-dependency - serverGroupRef: server-dependency + schedulerHints: + serverGroupRef: server-dependency userData: secretRef: server-dependency diff --git a/internal/controllers/server/tests/server-dependency/03-create-everything-but-server-group.yaml b/internal/controllers/server/tests/server-dependency/03-create-everything-but-server-group.yaml index 6483cf47f..f15e8960e 100644 --- a/internal/controllers/server/tests/server-dependency/03-create-everything-but-server-group.yaml +++ b/internal/controllers/server/tests/server-dependency/03-create-everything-but-server-group.yaml @@ -37,6 +37,7 @@ spec: flavorRef: server-dependency ports: - portRef: server-dependency - serverGroupRef: server-dependency + schedulerHints: + serverGroupRef: server-dependency userData: secretRef: server-dependency diff --git a/internal/controllers/server/tests/server-dependency/04-create-everything-but-userdata-secret.yaml b/internal/controllers/server/tests/server-dependency/04-create-everything-but-userdata-secret.yaml index bc9196a80..f8b8b4d02 100644 --- a/internal/controllers/server/tests/server-dependency/04-create-everything-but-userdata-secret.yaml +++ b/internal/controllers/server/tests/server-dependency/04-create-everything-but-userdata-secret.yaml @@ -35,6 +35,7 @@ spec: flavorRef: server-dependency ports: - portRef: server-dependency - serverGroupRef: server-dependency + schedulerHints: + serverGroupRef: server-dependency userData: secretRef: server-dependency diff --git a/internal/controllers/server/tests/server-dependency/05-create-everything-but-keypair.yaml b/internal/controllers/server/tests/server-dependency/05-create-everything-but-keypair.yaml index eb4776259..031ed37ac 100644 --- a/internal/controllers/server/tests/server-dependency/05-create-everything-but-keypair.yaml +++ b/internal/controllers/server/tests/server-dependency/05-create-everything-but-keypair.yaml @@ -27,7 +27,8 @@ spec: flavorRef: server-dependency ports: - portRef: server-dependency - serverGroupRef: server-dependency + schedulerHints: + serverGroupRef: server-dependency keypairRef: server-dependency userData: secretRef: server-dependency diff --git a/internal/controllers/server/tests/server-update/00-assert.yaml b/internal/controllers/server/tests/server-update/00-assert.yaml index 551244650..6964361e5 100644 --- a/internal/controllers/server/tests/server-update/00-assert.yaml +++ b/internal/controllers/server/tests/server-update/00-assert.yaml @@ -24,6 +24,7 @@ assertAll: - celExpr: "server.status.resource.serverGroups[0] == sg.status.id" - celExpr: "!has(server.status.resource.tags)" - celExpr: "!has(server.status.resource.volumes)" + - celExpr: "!has(server.status.resource.metadata)" - celExpr: "size(server.status.resource.interfaces) == 1" - celExpr: "server.status.resource.interfaces[0].portID == port.status.id" --- diff --git a/internal/controllers/server/tests/server-update/00-minimal-resource.yaml b/internal/controllers/server/tests/server-update/00-minimal-resource.yaml index 4a62a151a..95dca9e29 100644 --- a/internal/controllers/server/tests/server-update/00-minimal-resource.yaml +++ b/internal/controllers/server/tests/server-update/00-minimal-resource.yaml @@ -13,4 +13,5 @@ spec: flavorRef: server-update ports: - portRef: server-update - serverGroupRef: server-update \ No newline at end of file + schedulerHints: + serverGroupRef: server-update \ No newline at end of file diff --git a/internal/controllers/server/tests/server-update/01-assert.yaml b/internal/controllers/server/tests/server-update/01-assert.yaml index 473aecab0..db83497d8 100644 --- a/internal/controllers/server/tests/server-update/01-assert.yaml +++ b/internal/controllers/server/tests/server-update/01-assert.yaml @@ -54,6 +54,11 @@ status: tags: - tag1 - tag2 + metadata: + - key: environment + value: staging + - key: team + value: platform conditions: - type: Available status: "True" diff --git a/internal/controllers/server/tests/server-update/01-updated-resource.yaml b/internal/controllers/server/tests/server-update/01-updated-resource.yaml index 248b328a2..ae0cac6df 100644 --- a/internal/controllers/server/tests/server-update/01-updated-resource.yaml +++ b/internal/controllers/server/tests/server-update/01-updated-resource.yaml @@ -44,3 +44,8 @@ spec: tags: - tag1 - tag2 + metadata: + - key: environment + value: staging + - key: team + value: platform diff --git a/internal/controllers/server/tests/server-update/02-assert.yaml b/internal/controllers/server/tests/server-update/02-assert.yaml index 68beeb722..ec2db2777 100644 --- a/internal/controllers/server/tests/server-update/02-assert.yaml +++ b/internal/controllers/server/tests/server-update/02-assert.yaml @@ -32,6 +32,7 @@ assertAll: - celExpr: "server.status.resource.serverGroups[0] == sg.status.id" - celExpr: "!has(server.status.resource.tags)" - celExpr: "!has(server.status.resource.volumes)" + - celExpr: "!has(server.status.resource.metadata)" - celExpr: "!has(volume.status.resource.attachments)" - celExpr: "port1.status.resource.deviceID == server.status.id" - celExpr: "port1.status.resource.status == 'ACTIVE'" diff --git a/internal/controllers/volume/actuator.go b/internal/controllers/volume/actuator.go index 4e1e238f9..c1ec4335c 100644 --- a/internal/controllers/volume/actuator.go +++ b/internal/controllers/volume/actuator.go @@ -165,6 +165,20 @@ func (actuator volumeActuator) CreateResource(ctx context.Context, obj orcObject } } + // Resolve image dependency for bootable volumes + var imageID string + if resource.ImageRef != nil { + image, imageDepRS := imageDependency.GetDependency( + ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Image) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(imageDepRS) + if image != nil { + imageID = ptr.Deref(image.Status.ID, "") + } + } + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { return nil, reconcileStatus } @@ -181,6 +195,7 @@ func (actuator volumeActuator) CreateResource(ctx context.Context, obj orcObject Metadata: metadata, VolumeType: volumetypeID, AvailabilityZone: resource.AvailabilityZone, + ImageID: imageID, } osResource, err := actuator.osClient.CreateVolume(ctx, createOpts) diff --git a/internal/controllers/volume/controller.go b/internal/controllers/volume/controller.go index 276c4236c..531efbf3e 100644 --- a/internal/controllers/volume/controller.go +++ b/internal/controllers/volume/controller.go @@ -74,6 +74,18 @@ var volumetypeDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.Vo finalizer, externalObjectFieldOwner, ) +var imageDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.VolumeList, *orcv1alpha1.Image]( + "spec.resource.imageRef", + func(volume *orcv1alpha1.Volume) []string { + resource := volume.Spec.Resource + if resource == nil || resource.ImageRef == nil { + return nil + } + return []string{string(*resource.ImageRef)} + }, + finalizer, externalObjectFieldOwner, +) + // serverToVolumeMapFunc creates a mapping function that reconciles volumes when: // - a volume ID appears in server status but the volume doesn't have attachment info for that server // - a volume has attachment info for a server, but the server no longer lists that volume @@ -209,11 +221,19 @@ func (c volumeReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c return err } + imageWatchEventHandler, err := imageDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). Watches(&orcv1alpha1.VolumeType{}, volumetypeWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.VolumeType{})), ). + Watches(&orcv1alpha1.Image{}, imageWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Image{})), + ). Watches(&orcv1alpha1.Server{}, handler.EnqueueRequestsFromMapFunc(serverToVolumeMapFunc(ctx, k8sClient)), builder.WithPredicates(predicates.NewServerVolumesChanged(log)), ). @@ -221,6 +241,7 @@ func (c volumeReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c if err := errors.Join( volumetypeDependency.AddToManager(ctx, mgr), + imageDependency.AddToManager(ctx, mgr), credentialsDependency.AddToManager(ctx, mgr), credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), ); err != nil { diff --git a/internal/controllers/volume/status.go b/internal/controllers/volume/status.go index 064ef7575..96de129be 100644 --- a/internal/controllers/volume/status.go +++ b/internal/controllers/volume/status.go @@ -92,6 +92,13 @@ func (volumeStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osRes } } + // Extract image ID from volume_image_metadata if present. + // When a volume is created from an image, OpenStack stores the source + // image ID in the volume's metadata under "image_id". + if imageID, ok := osResource.VolumeImageMetadata["image_id"]; ok { + resourceStatus.WithImageID(imageID) + } + for k, v := range osResource.Metadata { resourceStatus.WithMetadata(orcapplyconfigv1alpha1.VolumeMetadataStatus(). WithName(k). diff --git a/internal/controllers/volume/tests/volume-create-bootable/00-assert.yaml b/internal/controllers/volume/tests/volume-create-bootable/00-assert.yaml new file mode 100644 index 000000000..c9444c53c --- /dev/null +++ b/internal/controllers/volume/tests/volume-create-bootable/00-assert.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: volume-create-bootable-image +status: + resource: + status: active +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: volume-create-bootable +status: + resource: + name: volume-create-bootable + size: 1 + status: available + bootable: true + encrypted: false + multiattach: false + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Volume + name: volume-create-bootable + ref: volume + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Image + name: volume-create-bootable-image + ref: image +assertAll: + - celExpr: "volume.status.id != ''" + - celExpr: "volume.status.resource.tenantID != ''" + - celExpr: "volume.status.resource.userID != ''" + - celExpr: "volume.status.resource.volumeType != ''" + - celExpr: "volume.status.resource.createdAt != ''" + - celExpr: "volume.status.resource.updatedAt != ''" + - celExpr: "volume.status.resource.imageID == image.status.id" diff --git a/internal/controllers/volume/tests/volume-create-bootable/00-create-resource.yaml b/internal/controllers/volume/tests/volume-create-bootable/00-create-resource.yaml new file mode 100644 index 000000000..7a20ca9d6 --- /dev/null +++ b/internal/controllers/volume/tests/volume-create-bootable/00-create-resource.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: volume-create-bootable-image +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + content: + diskFormat: raw + download: + url: https://github.com/k-orc/openstack-resource-controller/raw/690b760f49dfb61b173755e91cb51ed42472c7f3/internal/controllers/image/testdata/raw.img +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: volume-create-bootable +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + size: 1 + imageRef: volume-create-bootable-image diff --git a/internal/controllers/volume/tests/volume-create-bootable/00-secret.yaml b/internal/controllers/volume/tests/volume-create-bootable/00-secret.yaml new file mode 100644 index 000000000..f0fb63e85 --- /dev/null +++ b/internal/controllers/volume/tests/volume-create-bootable/00-secret.yaml @@ -0,0 +1,5 @@ +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 diff --git a/internal/controllers/volume/tests/volume-dependency/00-assert.yaml b/internal/controllers/volume/tests/volume-dependency/00-assert.yaml index 92782c001..7461a8dab 100644 --- a/internal/controllers/volume/tests/volume-dependency/00-assert.yaml +++ b/internal/controllers/volume/tests/volume-dependency/00-assert.yaml @@ -28,3 +28,18 @@ status: message: Waiting for VolumeType/volume-dependency to be created status: "True" reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: volume-dependency-no-image +status: + conditions: + - type: Available + message: Waiting for Image/volume-dependency-missing-image to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for Image/volume-dependency-missing-image to be created + status: "True" + reason: Progressing diff --git a/internal/controllers/volume/tests/volume-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/volume/tests/volume-dependency/00-create-resources-missing-deps.yaml index ac339b291..71224d5ff 100644 --- a/internal/controllers/volume/tests/volume-dependency/00-create-resources-missing-deps.yaml +++ b/internal/controllers/volume/tests/volume-dependency/00-create-resources-missing-deps.yaml @@ -23,3 +23,16 @@ spec: managementPolicy: managed resource: size: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Volume +metadata: + name: volume-dependency-no-image +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + size: 1 + imageRef: volume-dependency-missing-image diff --git a/internal/osclients/compute.go b/internal/osclients/compute.go index e40154150..43f4396d5 100644 --- a/internal/osclients/compute.go +++ b/internal/osclients/compute.go @@ -72,6 +72,7 @@ type ComputeClient interface { DeleteAttachedInterface(ctx context.Context, serverID, portID string) error ReplaceAllServerAttributesTags(ctx context.Context, resourceID string, opts tags.ReplaceAllOptsBuilder) ([]string, error) + ReplaceServerMetadata(ctx context.Context, serverID string, opts servers.MetadataOpts) (map[string]string, error) } type computeClient struct{ client *gophercloud.ServiceClient } @@ -187,6 +188,10 @@ func (c computeClient) ReplaceAllServerAttributesTags(ctx context.Context, resou return tags.ReplaceAll(ctx, c.client, resourceID, opts).Extract() } +func (c computeClient) ReplaceServerMetadata(ctx context.Context, serverID string, opts servers.MetadataOpts) (map[string]string, error) { + return servers.ResetMetadata(ctx, c.client, serverID, opts).Extract() +} + type computeErrorClient struct{ error } // NewComputeErrorClient returns a ComputeClient in which every method returns the given error. @@ -275,3 +280,7 @@ func (e computeErrorClient) DeleteAttachedInterface(_ context.Context, _, _ stri func (e computeErrorClient) ReplaceAllServerAttributesTags(_ context.Context, _ string, _ tags.ReplaceAllOptsBuilder) ([]string, error) { return nil, e.error } + +func (e computeErrorClient) ReplaceServerMetadata(_ context.Context, _ string, _ servers.MetadataOpts) (map[string]string, error) { + return nil, e.error +} diff --git a/internal/osclients/mock/compute.go b/internal/osclients/mock/compute.go index c22ab6984..f3e185299 100644 --- a/internal/osclients/mock/compute.go +++ b/internal/osclients/mock/compute.go @@ -324,6 +324,21 @@ func (mr *MockComputeClientMockRecorder) ReplaceAllServerAttributesTags(ctx, res return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceAllServerAttributesTags", reflect.TypeOf((*MockComputeClient)(nil).ReplaceAllServerAttributesTags), ctx, resourceID, opts) } +// ReplaceServerMetadata mocks base method. +func (m *MockComputeClient) ReplaceServerMetadata(ctx context.Context, serverID string, opts servers.MetadataOpts) (map[string]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReplaceServerMetadata", ctx, serverID, opts) + ret0, _ := ret[0].(map[string]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReplaceServerMetadata indicates an expected call of ReplaceServerMetadata. +func (mr *MockComputeClientMockRecorder) ReplaceServerMetadata(ctx, serverID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceServerMetadata", reflect.TypeOf((*MockComputeClient)(nil).ReplaceServerMetadata), ctx, serverID, opts) +} + // UpdateServer mocks base method. func (m *MockComputeClient) UpdateServer(ctx context.Context, id string, opts servers.UpdateOptsBuilder) (*servers.Server, error) { m.ctrl.T.Helper() diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/serverbootvolumespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/serverbootvolumespec.go new file mode 100644 index 000000000..e265034b8 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/serverbootvolumespec.go @@ -0,0 +1,52 @@ +/* +Copyright 2025 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" +) + +// ServerBootVolumeSpecApplyConfiguration represents a declarative configuration of the ServerBootVolumeSpec type for use +// with apply. +type ServerBootVolumeSpecApplyConfiguration struct { + VolumeRef *apiv1alpha1.KubernetesNameRef `json:"volumeRef,omitempty"` + Tag *string `json:"tag,omitempty"` +} + +// ServerBootVolumeSpecApplyConfiguration constructs a declarative configuration of the ServerBootVolumeSpec type for use with +// apply. +func ServerBootVolumeSpec() *ServerBootVolumeSpecApplyConfiguration { + return &ServerBootVolumeSpecApplyConfiguration{} +} + +// 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 *ServerBootVolumeSpecApplyConfiguration) WithVolumeRef(value apiv1alpha1.KubernetesNameRef) *ServerBootVolumeSpecApplyConfiguration { + b.VolumeRef = &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 *ServerBootVolumeSpecApplyConfiguration) WithTag(value string) *ServerBootVolumeSpecApplyConfiguration { + b.Tag = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/servermetadata.go b/pkg/clients/applyconfiguration/api/v1alpha1/servermetadata.go new file mode 100644 index 000000000..796ac43ea --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/servermetadata.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 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 + +// ServerMetadataApplyConfiguration represents a declarative configuration of the ServerMetadata type for use +// with apply. +type ServerMetadataApplyConfiguration struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +} + +// ServerMetadataApplyConfiguration constructs a declarative configuration of the ServerMetadata type for use with +// apply. +func ServerMetadata() *ServerMetadataApplyConfiguration { + return &ServerMetadataApplyConfiguration{} +} + +// WithKey sets the Key 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 Key field is set to the value of the last call. +func (b *ServerMetadataApplyConfiguration) WithKey(value string) *ServerMetadataApplyConfiguration { + b.Key = &value + return b +} + +// WithValue sets the Value 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 Value field is set to the value of the last call. +func (b *ServerMetadataApplyConfiguration) WithValue(value string) *ServerMetadataApplyConfiguration { + b.Value = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/servermetadatastatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/servermetadatastatus.go new file mode 100644 index 000000000..292dc8045 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/servermetadatastatus.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 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 + +// ServerMetadataStatusApplyConfiguration represents a declarative configuration of the ServerMetadataStatus type for use +// with apply. +type ServerMetadataStatusApplyConfiguration struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +} + +// ServerMetadataStatusApplyConfiguration constructs a declarative configuration of the ServerMetadataStatus type for use with +// apply. +func ServerMetadataStatus() *ServerMetadataStatusApplyConfiguration { + return &ServerMetadataStatusApplyConfiguration{} +} + +// WithKey sets the Key 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 Key field is set to the value of the last call. +func (b *ServerMetadataStatusApplyConfiguration) WithKey(value string) *ServerMetadataStatusApplyConfiguration { + b.Key = &value + return b +} + +// WithValue sets the Value 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 Value field is set to the value of the last call. +func (b *ServerMetadataStatusApplyConfiguration) WithValue(value string) *ServerMetadataStatusApplyConfiguration { + b.Value = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go index 5233713da..5e123f453 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcespec.go @@ -25,16 +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"` + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + ImageRef *apiv1alpha1.KubernetesNameRef `json:"imageRef,omitempty"` + FlavorRef *apiv1alpha1.KubernetesNameRef `json:"flavorRef,omitempty"` + BootVolume *ServerBootVolumeSpecApplyConfiguration `json:"bootVolume,omitempty"` + UserData *UserDataSpecApplyConfiguration `json:"userData,omitempty"` + Ports []ServerPortSpecApplyConfiguration `json:"ports,omitempty"` + Volumes []ServerVolumeSpecApplyConfiguration `json:"volumes,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"` + SchedulerHints *ServerSchedulerHintsApplyConfiguration `json:"schedulerHints,omitempty"` } // ServerResourceSpecApplyConfiguration constructs a declarative configuration of the ServerResourceSpec type for use with @@ -67,6 +70,14 @@ func (b *ServerResourceSpecApplyConfiguration) WithFlavorRef(value apiv1alpha1.K return b } +// WithBootVolume sets the BootVolume 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 BootVolume field is set to the value of the last call. +func (b *ServerResourceSpecApplyConfiguration) WithBootVolume(value *ServerBootVolumeSpecApplyConfiguration) *ServerResourceSpecApplyConfiguration { + b.BootVolume = value + return b +} + // WithUserData sets the UserData 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 UserData field is set to the value of the last call. @@ -101,14 +112,6 @@ func (b *ServerResourceSpecApplyConfiguration) WithVolumes(values ...*ServerVolu 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. -func (b *ServerResourceSpecApplyConfiguration) WithServerGroupRef(value apiv1alpha1.KubernetesNameRef) *ServerResourceSpecApplyConfiguration { - b.ServerGroupRef = &value - return b -} - // WithAvailabilityZone sets the AvailabilityZone 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 AvailabilityZone field is set to the value of the last call. @@ -134,3 +137,32 @@ func (b *ServerResourceSpecApplyConfiguration) WithTags(values ...apiv1alpha1.Se } return b } + +// WithMetadata adds the given value to the Metadata 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 Metadata field. +func (b *ServerResourceSpecApplyConfiguration) WithMetadata(values ...*ServerMetadataApplyConfiguration) *ServerResourceSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMetadata") + } + b.Metadata = append(b.Metadata, *values[i]) + } + return b +} + +// WithConfigDrive sets the ConfigDrive 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 ConfigDrive field is set to the value of the last call. +func (b *ServerResourceSpecApplyConfiguration) WithConfigDrive(value bool) *ServerResourceSpecApplyConfiguration { + b.ConfigDrive = &value + return b +} + +// WithSchedulerHints sets the SchedulerHints 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 SchedulerHints field is set to the value of the last call. +func (b *ServerResourceSpecApplyConfiguration) WithSchedulerHints(value *ServerSchedulerHintsApplyConfiguration) *ServerResourceSpecApplyConfiguration { + b.SchedulerHints = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcestatus.go index 119583f20..60a359afb 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcestatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/serverresourcestatus.go @@ -30,6 +30,8 @@ type ServerResourceStatusApplyConfiguration struct { Volumes []ServerVolumeStatusApplyConfiguration `json:"volumes,omitempty"` Interfaces []ServerInterfaceStatusApplyConfiguration `json:"interfaces,omitempty"` Tags []string `json:"tags,omitempty"` + Metadata []ServerMetadataStatusApplyConfiguration `json:"metadata,omitempty"` + ConfigDrive *bool `json:"configDrive,omitempty"` } // ServerResourceStatusApplyConfiguration constructs a declarative configuration of the ServerResourceStatus type for use with @@ -123,3 +125,24 @@ func (b *ServerResourceStatusApplyConfiguration) WithTags(values ...string) *Ser } return b } + +// WithMetadata adds the given value to the Metadata 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 Metadata field. +func (b *ServerResourceStatusApplyConfiguration) WithMetadata(values ...*ServerMetadataStatusApplyConfiguration) *ServerResourceStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMetadata") + } + b.Metadata = append(b.Metadata, *values[i]) + } + return b +} + +// WithConfigDrive sets the ConfigDrive 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 ConfigDrive field is set to the value of the last call. +func (b *ServerResourceStatusApplyConfiguration) WithConfigDrive(value bool) *ServerResourceStatusApplyConfiguration { + b.ConfigDrive = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/serverschedulerhints.go b/pkg/clients/applyconfiguration/api/v1alpha1/serverschedulerhints.go new file mode 100644 index 000000000..d0e89425b --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/serverschedulerhints.go @@ -0,0 +1,118 @@ +/* +Copyright 2025 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" +) + +// ServerSchedulerHintsApplyConfiguration represents a declarative configuration of the ServerSchedulerHints type for use +// with apply. +type ServerSchedulerHintsApplyConfiguration struct { + ServerGroupRef *apiv1alpha1.KubernetesNameRef `json:"serverGroupRef,omitempty"` + DifferentHostServerRefs []apiv1alpha1.KubernetesNameRef `json:"differentHostServerRefs,omitempty"` + SameHostServerRefs []apiv1alpha1.KubernetesNameRef `json:"sameHostServerRefs,omitempty"` + Query *string `json:"query,omitempty"` + TargetCell *string `json:"targetCell,omitempty"` + DifferentCell []string `json:"differentCell,omitempty"` + BuildNearHostIP *string `json:"buildNearHostIP,omitempty"` + AdditionalProperties map[string]string `json:"additionalProperties,omitempty"` +} + +// ServerSchedulerHintsApplyConfiguration constructs a declarative configuration of the ServerSchedulerHints type for use with +// apply. +func ServerSchedulerHints() *ServerSchedulerHintsApplyConfiguration { + return &ServerSchedulerHintsApplyConfiguration{} +} + +// 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. +func (b *ServerSchedulerHintsApplyConfiguration) WithServerGroupRef(value apiv1alpha1.KubernetesNameRef) *ServerSchedulerHintsApplyConfiguration { + b.ServerGroupRef = &value + return b +} + +// WithDifferentHostServerRefs adds the given value to the DifferentHostServerRefs 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 DifferentHostServerRefs field. +func (b *ServerSchedulerHintsApplyConfiguration) WithDifferentHostServerRefs(values ...apiv1alpha1.KubernetesNameRef) *ServerSchedulerHintsApplyConfiguration { + for i := range values { + b.DifferentHostServerRefs = append(b.DifferentHostServerRefs, values[i]) + } + return b +} + +// WithSameHostServerRefs adds the given value to the SameHostServerRefs 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 SameHostServerRefs field. +func (b *ServerSchedulerHintsApplyConfiguration) WithSameHostServerRefs(values ...apiv1alpha1.KubernetesNameRef) *ServerSchedulerHintsApplyConfiguration { + for i := range values { + b.SameHostServerRefs = append(b.SameHostServerRefs, values[i]) + } + return b +} + +// WithQuery sets the Query 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 Query field is set to the value of the last call. +func (b *ServerSchedulerHintsApplyConfiguration) WithQuery(value string) *ServerSchedulerHintsApplyConfiguration { + b.Query = &value + return b +} + +// WithTargetCell sets the TargetCell 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 TargetCell field is set to the value of the last call. +func (b *ServerSchedulerHintsApplyConfiguration) WithTargetCell(value string) *ServerSchedulerHintsApplyConfiguration { + b.TargetCell = &value + return b +} + +// WithDifferentCell adds the given value to the DifferentCell 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 DifferentCell field. +func (b *ServerSchedulerHintsApplyConfiguration) WithDifferentCell(values ...string) *ServerSchedulerHintsApplyConfiguration { + for i := range values { + b.DifferentCell = append(b.DifferentCell, values[i]) + } + return b +} + +// WithBuildNearHostIP sets the BuildNearHostIP 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 BuildNearHostIP field is set to the value of the last call. +func (b *ServerSchedulerHintsApplyConfiguration) WithBuildNearHostIP(value string) *ServerSchedulerHintsApplyConfiguration { + b.BuildNearHostIP = &value + return b +} + +// WithAdditionalProperties puts the entries into the AdditionalProperties field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the AdditionalProperties field, +// overwriting an existing map entries in AdditionalProperties field with the same key. +func (b *ServerSchedulerHintsApplyConfiguration) WithAdditionalProperties(entries map[string]string) *ServerSchedulerHintsApplyConfiguration { + if b.AdditionalProperties == nil && len(entries) > 0 { + b.AdditionalProperties = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.AdditionalProperties[k] = v + } + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcespec.go index 7386c2714..e345fc165 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcespec.go @@ -31,6 +31,7 @@ type VolumeResourceSpecApplyConfiguration struct { VolumeTypeRef *apiv1alpha1.KubernetesNameRef `json:"volumeTypeRef,omitempty"` AvailabilityZone *string `json:"availabilityZone,omitempty"` Metadata []VolumeMetadataApplyConfiguration `json:"metadata,omitempty"` + ImageRef *apiv1alpha1.KubernetesNameRef `json:"imageRef,omitempty"` } // VolumeResourceSpecApplyConfiguration constructs a declarative configuration of the VolumeResourceSpec type for use with @@ -91,3 +92,11 @@ func (b *VolumeResourceSpecApplyConfiguration) WithMetadata(values ...*VolumeMet } 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 *VolumeResourceSpecApplyConfiguration) WithImageRef(value apiv1alpha1.KubernetesNameRef) *VolumeResourceSpecApplyConfiguration { + b.ImageRef = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcestatus.go index d3113778e..c1ec97e00 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcestatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/volumeresourcestatus.go @@ -38,6 +38,7 @@ type VolumeResourceStatusApplyConfiguration struct { Metadata []VolumeMetadataStatusApplyConfiguration `json:"metadata,omitempty"` UserID *string `json:"userID,omitempty"` Bootable *bool `json:"bootable,omitempty"` + ImageID *string `json:"imageID,omitempty"` Encrypted *bool `json:"encrypted,omitempty"` ReplicationStatus *string `json:"replicationStatus,omitempty"` ConsistencyGroupID *string `json:"consistencyGroupID,omitempty"` @@ -168,6 +169,14 @@ func (b *VolumeResourceStatusApplyConfiguration) WithBootable(value bool) *Volum return b } +// WithImageID sets the ImageID 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 ImageID field is set to the value of the last call. +func (b *VolumeResourceStatusApplyConfiguration) WithImageID(value string) *VolumeResourceStatusApplyConfiguration { + b.ImageID = &value + return b +} + // WithEncrypted sets the Encrypted 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 Encrypted 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 5b5cb5142..2a4f3fd67 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -2163,6 +2163,15 @@ 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.ServerBootVolumeSpec + map: + fields: + - name: tag + type: + scalar: string + - name: volumeRef + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerFilter map: fields: @@ -2349,6 +2358,24 @@ var schemaYAML = typed.YAMLObject(`types: - name: portState type: scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerMetadata + map: + fields: + - name: key + type: + scalar: string + - name: value + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerMetadataStatus + map: + fields: + - name: key + type: + scalar: string + - name: value + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerPortSpec map: fields: @@ -2361,6 +2388,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: availabilityZone type: scalar: string + - name: bootVolume + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerBootVolumeSpec + - name: configDrive + type: + scalar: boolean - name: flavorRef type: scalar: string @@ -2370,6 +2403,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: keypairRef type: scalar: string + - name: metadata + type: + list: + elementType: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerMetadata + elementRelationship: atomic - name: name type: scalar: string @@ -2379,9 +2418,9 @@ var schemaYAML = typed.YAMLObject(`types: elementType: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerPortSpec elementRelationship: atomic - - name: serverGroupRef + - name: schedulerHints type: - scalar: string + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerSchedulerHints - name: tags type: list: @@ -2403,6 +2442,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: availabilityZone type: scalar: string + - name: configDrive + type: + scalar: boolean - name: hostID type: scalar: string @@ -2415,6 +2457,12 @@ var schemaYAML = typed.YAMLObject(`types: elementType: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerInterfaceStatus elementRelationship: atomic + - name: metadata + type: + list: + elementType: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerMetadataStatus + elementRelationship: atomic - name: name type: scalar: string @@ -2439,6 +2487,44 @@ var schemaYAML = typed.YAMLObject(`types: elementType: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerVolumeStatus elementRelationship: atomic +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerSchedulerHints + map: + fields: + - name: additionalProperties + type: + map: + elementType: + scalar: string + - name: buildNearHostIP + type: + scalar: string + - name: differentCell + type: + list: + elementType: + scalar: string + elementRelationship: associative + - name: differentHostServerRefs + type: + list: + elementType: + scalar: string + elementRelationship: associative + - name: query + type: + scalar: string + - name: sameHostServerRefs + type: + list: + elementType: + scalar: string + elementRelationship: associative + - name: serverGroupRef + type: + scalar: string + - name: targetCell + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ServerSpec map: fields: @@ -2953,6 +3039,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: description type: scalar: string + - name: imageRef + type: + scalar: string - name: metadata type: list: @@ -3001,6 +3090,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: host type: scalar: string + - name: imageID + type: + scalar: string - name: metadata type: list: diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index e3166fefe..9540b6aec 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -266,6 +266,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.SecurityGroupStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Server"): return &apiv1alpha1.ServerApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ServerBootVolumeSpec"): + return &apiv1alpha1.ServerBootVolumeSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerFilter"): return &apiv1alpha1.ServerFilterApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerGroup"): @@ -292,12 +294,18 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.ServerInterfaceFixedIPApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerInterfaceStatus"): return &apiv1alpha1.ServerInterfaceStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ServerMetadata"): + return &apiv1alpha1.ServerMetadataApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ServerMetadataStatus"): + return &apiv1alpha1.ServerMetadataStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerPortSpec"): return &apiv1alpha1.ServerPortSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerResourceSpec"): return &apiv1alpha1.ServerResourceSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerResourceStatus"): return &apiv1alpha1.ServerResourceStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ServerSchedulerHints"): + return &apiv1alpha1.ServerSchedulerHintsApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerSpec"): return &apiv1alpha1.ServerSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerStatus"): diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 23b3b2403..b3ebf5161 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -1627,8 +1627,10 @@ _Appears in:_ - [RouterResourceSpec](#routerresourcespec) - [SecurityGroupFilter](#securitygroupfilter) - [SecurityGroupResourceSpec](#securitygroupresourcespec) +- [ServerBootVolumeSpec](#serverbootvolumespec) - [ServerPortSpec](#serverportspec) - [ServerResourceSpec](#serverresourcespec) +- [ServerSchedulerHints](#serverschedulerhints) - [ServerVolumeSpec](#servervolumespec) - [SubnetFilter](#subnetfilter) - [SubnetResourceSpec](#subnetresourcespec) @@ -3028,6 +3030,24 @@ Server is the Schema for an ORC resource. | `status` _[ServerStatus](#serverstatus)_ | status defines the observed state of the resource. | | | +#### ServerBootVolumeSpec + + + +ServerBootVolumeSpec defines the boot volume for boot-from-volume server creation. +When specified, the server boots from this volume instead of an image. + + + +_Appears in:_ +- [ServerResourceSpec](#serverresourcespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `volumeRef` _[KubernetesNameRef](#kubernetesnameref)_ | volumeRef is a reference to a Volume object. The volume must be
bootable (created from an image) and available before server creation. | | MaxLength: 253
MinLength: 1
| +| `tag` _string_ | tag is the device tag applied to the volume. | | MaxLength: 255
| + + #### ServerFilter @@ -3291,6 +3311,40 @@ _Appears in:_ | `fixedIPs` _[ServerInterfaceFixedIP](#serverinterfacefixedip) array_ | fixedIPs is the list of fixed IP addresses assigned to the interface. | | MaxItems: 32
| +#### ServerMetadata + + + +ServerMetadata represents a key-value pair for server metadata. + + + +_Appears in:_ +- [ServerResourceSpec](#serverresourcespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `key` _string_ | key is the metadata key. | | MaxLength: 255
MinLength: 1
| +| `value` _string_ | value is the metadata value. | | MaxLength: 255
MinLength: 1
| + + +#### ServerMetadataStatus + + + +ServerMetadataStatus represents a key-value pair for server metadata in status. + + + +_Appears in:_ +- [ServerResourceStatus](#serverresourcestatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `key` _string_ | key is the metadata key. | | MaxLength: 255
| +| `value` _string_ | value is the metadata value. | | MaxLength: 255
| + + #### ServerPortSpec @@ -3323,15 +3377,18 @@ _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 field is required unless bootVolume is specified for boot-from-volume. | | MaxLength: 253
MinLength: 1
| | `flavorRef` _[KubernetesNameRef](#kubernetesnameref)_ | flavorRef references the flavor to use for the server instance. | | MaxLength: 253
MinLength: 1
| +| `bootVolume` _[ServerBootVolumeSpec](#serverbootvolumespec)_ | bootVolume specifies a volume to boot from instead of an image.
When specified, imageRef must be omitted. The volume must be
bootable (created from an image using imageRef in the Volume spec). | | | | `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
| -| `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
| | `tags` _[ServerTag](#servertag) array_ | tags is a list of tags which will be applied to the server. | | MaxItems: 50
MaxLength: 80
MinLength: 1
| +| `metadata` _[ServerMetadata](#servermetadata) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 128
| +| `configDrive` _boolean_ | configDrive specifies whether to attach a config drive to the server.
When true, configuration data will be available via a special drive
instead of the metadata service. | | | +| `schedulerHints` _[ServerSchedulerHints](#serverschedulerhints)_ | schedulerHints provides hints to the Nova scheduler for server placement. | | | #### ServerResourceStatus @@ -3356,6 +3413,31 @@ _Appears in:_ | `volumes` _[ServerVolumeStatus](#servervolumestatus) array_ | volumes contains the volumes attached to the server. | | MaxItems: 64
| | `interfaces` _[ServerInterfaceStatus](#serverinterfacestatus) array_ | interfaces contains the list of interfaces attached to the server. | | MaxItems: 64
| | `tags` _string array_ | tags is the list of tags on the resource. | | MaxItems: 50
items:MaxLength: 1024
| +| `metadata` _[ServerMetadataStatus](#servermetadatastatus) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 128
| +| `configDrive` _boolean_ | configDrive indicates whether the server was booted with a config drive. | | | + + +#### ServerSchedulerHints + + + +ServerSchedulerHints provides hints to the Nova scheduler for server placement. + + + +_Appears in:_ +- [ServerResourceSpec](#serverresourcespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serverGroupRef` _[KubernetesNameRef](#kubernetesnameref)_ | serverGroupRef is a reference to a ServerGroup object. The server will be
scheduled on a host in the specified server group. | | MaxLength: 253
MinLength: 1
| +| `differentHostServerRefs` _[KubernetesNameRef](#kubernetesnameref) array_ | differentHostServerRefs is a list of references to Server objects.
The server will be scheduled on a different host than all specified servers. | | MaxItems: 64
MaxLength: 253
MinLength: 1
| +| `sameHostServerRefs` _[KubernetesNameRef](#kubernetesnameref) array_ | sameHostServerRefs is a list of references to Server objects.
The server will be scheduled on the same host as all specified servers. | | MaxItems: 64
MaxLength: 253
MinLength: 1
| +| `query` _string_ | query is a conditional statement that results in compute nodes
able to host the server. | | MaxLength: 1024
| +| `targetCell` _string_ | targetCell is a cell name where the server will be placed. | | MaxLength: 255
| +| `differentCell` _string array_ | differentCell is a list of cell names where the server should not
be placed. | | MaxItems: 64
items:MaxLength: 1024
| +| `buildNearHostIP` _string_ | buildNearHostIP specifies a subnet of compute nodes to host the server. | | MaxLength: 255
| +| `additionalProperties` _object (keys:string, values:string)_ | additionalProperties is a map of arbitrary key/value pairs that are
not validated by Nova. | | | #### ServerSpec @@ -3930,6 +4012,7 @@ _Appears in:_ | `volumeTypeRef` _[KubernetesNameRef](#kubernetesnameref)_ | volumeTypeRef is a reference to the ORC VolumeType which this resource is associated with. | | MaxLength: 253
MinLength: 1
| | `availabilityZone` _string_ | availabilityZone is the availability zone in which to create the volume. | | MaxLength: 255
| | `metadata` _[VolumeMetadata](#volumemetadata) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64
| +| `imageRef` _[KubernetesNameRef](#kubernetesnameref)_ | imageRef is a reference to an ORC Image. If specified, creates a
bootable volume from this image. The volume size must be >= the
image's min_disk requirement. | | MaxLength: 253
MinLength: 1
| #### VolumeResourceStatus @@ -3958,6 +4041,7 @@ _Appears in:_ | `metadata` _[VolumeMetadataStatus](#volumemetadatastatus) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64
| | `userID` _string_ | userID is the ID of the user who created the volume. | | MaxLength: 1024
| | `bootable` _boolean_ | bootable indicates whether this is a bootable volume. | | | +| `imageID` _string_ | imageID is the ID of the image this volume was created from, if any. | | MaxLength: 1024
| | `encrypted` _boolean_ | encrypted denotes if the volume is encrypted. | | | | `replicationStatus` _string_ | replicationStatus is the status of replication. | | MaxLength: 1024
| | `consistencyGroupID` _string_ | consistencyGroupID is the consistency group ID. | | MaxLength: 1024
|