Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/v1alpha1/volume_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions cmd/models-schema/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions config/crd/bases/openstack.k-orc.cloud_volumes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions config/samples/openstack_v1alpha1_volume_bootable.yaml
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions internal/controllers/volume/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
"github.com/k-orc/openstack-resource-controller/v2/internal/logging"
"github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
"github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
)

Expand Down Expand Up @@ -165,6 +166,17 @@ func (actuator volumeActuator) CreateResource(ctx context.Context, obj orcObject
}
}

// Resolve image dependency for bootable volumes
image, imageDepRS := dependency.FetchDependency(
ctx, actuator.k8sClient, obj.Namespace,
resource.ImageRef, "Image",
func(dep *orcv1alpha1.Image) bool {
return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
},
)
reconcileStatus = reconcileStatus.WithReconcileStatus(imageDepRS)
imageID := ptr.Deref(image.Status.ID, "")

if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
return nil, reconcileStatus
}
Expand All @@ -181,6 +193,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)
Expand Down
22 changes: 22 additions & 0 deletions internal/controllers/volume/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ var volumetypeDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.Vo
finalizer, externalObjectFieldOwner,
)

// No deletion guard for image, because images can be safely deleted while
// referenced by a volume
var imageDependency = dependency.NewDependency[*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)}
},
)

// 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
Expand Down Expand Up @@ -209,18 +222,27 @@ 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)),
).
For(&orcv1alpha1.Volume{})

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 {
Expand Down
7 changes: 7 additions & 0 deletions internal/controllers/volume/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this test is not necessary because we're already checking the creation of a bootable volume in the volume-dependency test. We will also use bootable volumes when testing for servers booted from volumes soon. What do you say we drop the volume-create-bootable test?

Copy link
Contributor Author

@eshulman2 eshulman2 Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should leave this one and remove the dependency as there is no deletion guard for image so I feel it would make more sense keeping this and removing the dependency, WDYT?

Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pkg/clients/applyconfiguration/internal/internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions website/docs/crd-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3972,6 +3972,7 @@ _Appears in:_
| `volumeTypeRef` _[KubernetesNameRef](#kubernetesnameref)_ | volumeTypeRef is a reference to the ORC VolumeType which this resource is associated with. | | MaxLength: 253 <br />MinLength: 1 <br /> |
| `availabilityZone` _string_ | availabilityZone is the availability zone in which to create the volume. | | MaxLength: 255 <br /> |
| `metadata` _[VolumeMetadata](#volumemetadata) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64 <br /> |
| `imageRef` _[KubernetesNameRef](#kubernetesnameref)_ | imageRef is a reference to an ORC Image. If specified, creates a<br />bootable volume from this image. The volume size must be >= the<br />image's min_disk requirement. | | MaxLength: 253 <br />MinLength: 1 <br /> |


#### VolumeResourceStatus
Expand Down Expand Up @@ -4000,6 +4001,7 @@ _Appears in:_
| `metadata` _[VolumeMetadataStatus](#volumemetadatastatus) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64 <br /> |
| `userID` _string_ | userID is the ID of the user who created the volume. | | MaxLength: 1024 <br /> |
| `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 <br /> |
| `encrypted` _boolean_ | encrypted denotes if the volume is encrypted. | | |
| `replicationStatus` _string_ | replicationStatus is the status of replication. | | MaxLength: 1024 <br /> |
| `consistencyGroupID` _string_ | consistencyGroupID is the consistency group ID. | | MaxLength: 1024 <br /> |
Expand Down