Skip to content
Merged
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
1 change: 1 addition & 0 deletions config/samples/openstack_v1alpha1_volume.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ spec:
description: Sample Volume
size: 100
volumeTypeRef: my-volume-type
imageRef: ubuntu-2404
metadata:
key1: value1
key2: value2
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
15 changes: 15 additions & 0 deletions internal/controllers/volume/tests/volume-dependency/00-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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-image to be created
status: "False"
reason: Progressing
- type: Progressing
message: Waiting for Image/volume-dependency-image to be created
status: "True"
reason: Progressing
Original file line number Diff line number Diff line change
Expand Up @@ -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-image
17 changes: 17 additions & 0 deletions internal/controllers/volume/tests/volume-dependency/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,20 @@ status:
message: OpenStack resource is up to date
status: "False"
reason: Success
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: Volume
metadata:
name: volume-dependency-no-image
status:
resource:
size: 1
status: available
bootable: true
conditions:
- type: Available
status: "True"
reason: Success
- type: Progressing
status: "False"
reason: Success
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,18 @@ spec:
secretName: openstack-clouds
managementPolicy: managed
resource: {}
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: Image
metadata:
name: volume-dependency-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
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ assertAll:
- celExpr: "'openstack.k-orc.cloud/volume' in volumetype.metadata.finalizers"
- celExpr: "secret.metadata.deletionTimestamp != 0"
- celExpr: "'openstack.k-orc.cloud/volume' in secret.metadata.finalizers"
commands:
# Image is a creation dependency, so it should be deleted immediately
- script: "! kubectl get image volume-dependency-image --namespace $NAMESPACE"
skipLogOutput: true
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ commands:
namespaced: true
- command: kubectl delete secret volume-dependency --wait=false
namespaced: true
- command: kubectl delete image volume-dependency-image --wait=false
namespaced: true
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ delete:
- apiVersion: openstack.k-orc.cloud/v1alpha1
kind: Volume
name: volume-dependency-no-volumetype
- apiVersion: openstack.k-orc.cloud/v1alpha1
kind: Volume
name: volume-dependency-no-image
6 changes: 4 additions & 2 deletions internal/controllers/volume/tests/volume-dependency/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ Create the missing dependencies and make and verify all the Volumes are availabl

## Step 02

Delete all the dependencies and check that ORC prevents deletion since there is still a resource that depends on them.
Delete all the dependencies and check:
- VolumeType and Secret have finalizers preventing deletion (hard dependencies)
- Image is deleted immediately (soft dependency - no finalizer)

## Step 03

Delete the Volumes and validate that all resources are gone.
Delete the Volumes and validate that VolumeType and Secret are now gone.

## Reference

Expand Down

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