Skip to content

Commit e3c4521

Browse files
committed
image: Replace controllerOptions with managementPolicy
Remove controllerOptions in favour of managementPolicy. Adoption is now handled explicitly in the `import` field.
1 parent 94ba718 commit e3c4521

File tree

32 files changed

+1712
-972
lines changed

32 files changed

+1712
-972
lines changed

internal/controllers/image/controller.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ import (
3535
ctrlutil "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/controllers"
3636
)
3737

38+
const (
39+
Finalizer = "openstack.k-orc.cloud/image"
40+
41+
FieldOwner = "openstack.k-orc.cloud/imagecontroller"
42+
// Field owner of the object finalizer.
43+
SSAFinalizerTxn = "finalizer"
44+
// Field owner of transient status.
45+
SSAStatusTxn = "status"
46+
// Field owner of persistent id field.
47+
SSAIDTxn = "id"
48+
)
49+
50+
// ssaFieldOwner returns the field owner for a specific named SSA transaction.
51+
func ssaFieldOwner(txn string) client.FieldOwner {
52+
return client.FieldOwner(FieldOwner + "/" + txn)
53+
}
54+
3855
const (
3956
// The time to wait before reconciling again when we are expecting glance to finish some task and update status.
4057
waitForGlanceImageStatusUpdate = 15 * time.Second

internal/controllers/image/reconcile.go

Lines changed: 126 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcImage *orcv
7474
log := ctrl.LoggerFrom(ctx)
7575
log.V(3).Info("Reconciling image")
7676

77-
if !controllerutil.ContainsFinalizer(orcImage, orcv1alpha1.ImageControllerFinalizer) {
78-
return ctrl.Result{}, r.updateObject(ctx, orcImage)
77+
if !controllerutil.ContainsFinalizer(orcImage, Finalizer) {
78+
return ctrl.Result{}, r.setFinalizer(ctx, orcImage)
7979
}
8080

8181
var statusOpts []updateStatusOpt
@@ -90,41 +90,81 @@ func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcImage *orcv
9090
}
9191

9292
err = errors.Join(err, r.updateStatus(ctx, orcImage, statusOpts...))
93+
94+
var terminalError *capoerrors.TerminalError
95+
if errors.As(err, &terminalError) {
96+
log.Error(err, "not scheduling further reconciles for terminal error")
97+
err = nil
98+
}
9399
}()
94100

95101
imageClient, err := r.getImageClient(ctx, orcImage)
96102
if err != nil {
97103
return ctrl.Result{}, err
98104
}
99105

100-
var glanceImage *images.Image
101-
glanceImage, err = getGlanceImage(ctx, orcImage, imageClient)
102-
if err != nil {
103-
if capoerrors.IsNotFound(err) {
104-
// An image we previously created has been deleted unexpected. We can't recover from this.
105-
err = capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "image has been deleted from glance")
106+
glanceImage, err := func() (*images.Image, error) {
107+
if orcImage.Status.ID != nil {
108+
log.V(4).Info("Fetching existing glance image", "ID", *orcImage.Status.ID)
109+
110+
image, err := imageClient.GetImage(*orcImage.Status.ID)
111+
if capoerrors.IsNotFound(err) {
112+
// An image we previously referenced has been deleted unexpectedly. We can't recover from this.
113+
err = capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "image has been deleted from glance")
114+
}
115+
return image, err
116+
}
117+
118+
if orcImage.Spec.Import != nil {
119+
log.V(4).Info("Importing existing glance image")
120+
121+
if orcImage.Spec.Import.ID != nil {
122+
image, err := imageClient.GetImage(*orcImage.Spec.Import.ID)
123+
if capoerrors.IsNotFound(err) {
124+
// We assume that an image imported by ID must already exist. It's a terminal error if it doesn't.
125+
err = capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonUnrecoverableError, "referenced image does not exist in glance")
126+
}
127+
return image, err
128+
}
129+
130+
listOpts := listOptsFromImportFilter(orcImage.Spec.Import.Filter)
131+
return getGlanceImageFromList(ctx, listOpts, imageClient)
132+
133+
// TODO: When we support 'import and manage' we need to implement
134+
// setting spec.resource from the discovered glance image here.
106135
}
107-
return ctrl.Result{}, err
108-
}
109136

110-
if orcImage.GetControllerOptions().GetOnCreate() == orcv1alpha1.ControllerOptionsOnCreateAdopt && glanceImage == nil {
111-
log.V(3).Info("Image does not yet exist", "onCreate", orcv1alpha1.ControllerOptionsOnCreateAdopt)
112-
addStatus(withProgressMessage("Waiting for glance image to be created externally"))
137+
log.V(4).Info("Checking for previously created image")
113138

114-
return ctrl.Result{
115-
RequeueAfter: waitForGlanceImageStatusUpdate,
116-
}, err
139+
listOpts := listOptsFromCreation(orcImage)
140+
return getGlanceImageFromList(ctx, listOpts, imageClient)
141+
}()
142+
if err != nil {
143+
return ctrl.Result{}, err
117144
}
118145

119146
if glanceImage == nil {
147+
if orcImage.Spec.Import != nil {
148+
log.V(3).Info("Image does not yet exist")
149+
addStatus(withProgressMessage("Waiting for glance image to be created externally"))
150+
151+
return ctrl.Result{RequeueAfter: waitForGlanceImageStatusUpdate}, err
152+
}
153+
120154
glanceImage, err = createImage(ctx, orcImage, imageClient)
121155
if err != nil {
122156
return ctrl.Result{}, err
123157
}
124158
}
125159
addStatus(withGlanceImage(glanceImage))
126160

127-
log = log.WithValues("imageID", glanceImage.ID)
161+
if orcImage.Status.ID == nil {
162+
if err := r.setStatusID(ctx, orcImage, glanceImage.ID); err != nil {
163+
return ctrl.Result{}, err
164+
}
165+
}
166+
167+
log = log.WithValues("ID", glanceImage.ID)
128168
ctx = ctrl.LoggerInto(ctx, log)
129169

130170
log.V(4).Info("Got glance image", "status", glanceImage.Status)
@@ -143,8 +183,8 @@ func (r *orcImageReconciler) reconcileNormal(ctx context.Context, orcImage *orcv
143183

144184
// Newly created image, waiting for upload, or... previous upload was interrupted and has now reset
145185
case images.ImageStatusQueued:
146-
// Don't attempt image creation if we're only adopting
147-
if orcImage.GetControllerOptions().GetOnCreate() == orcv1alpha1.ControllerOptionsOnCreateAdopt {
186+
// Don't attempt image creation if we're not managing the image
187+
if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyUnmanaged {
148188
addStatus(withProgressMessage("Waiting for glance image content to be uploaded externally"))
149189

150190
return ctrl.Result{
@@ -213,7 +253,14 @@ func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcImage *orcv
213253
}
214254
}()
215255

216-
if orcImage.GetControllerOptions().GetOnDelete() == orcv1alpha1.ControllerOptionsOnDeleteDelete {
256+
// We won't delete the resource for an unmanaged object, or if deletePolicy is detach
257+
if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyUnmanaged || orcImage.Spec.ManagedOptions.GetDeletePolicy() == orcv1alpha1.DeletePolicyDetach {
258+
logPolicy := []any{"managementPolicy", orcImage.Spec.ManagementPolicy}
259+
if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyManaged {
260+
logPolicy = append(logPolicy, "deletePolicy", orcImage.Spec.ManagedOptions.GetDeletePolicy())
261+
}
262+
log.V(4).Info("Not deleting Glance image due to policy", logPolicy...)
263+
} else {
217264
imageClient, err := r.getImageClient(ctx, orcImage)
218265
if err != nil {
219266
return ctrl.Result{}, err
@@ -235,14 +282,15 @@ func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcImage *orcv
235282
}
236283
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
237284
}
285+
286+
log.V(4).Info("Image is deleted")
238287
}
239288

240289
deleted = true
241-
log.V(4).Info("Image is deleted")
242290

243-
// Clear owned fields on the base resource, including the finalizer
291+
// Clear the finalizer
244292
applyConfig := orcapplyconfigv1alpha1.Image(orcImage.Name, orcImage.Namespace).WithUID(orcImage.UID)
245-
return ctrl.Result{}, r.client.Patch(ctx, orcImage, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, client.FieldOwner(orcv1alpha1.ImageControllerFieldOwner))
293+
return ctrl.Result{}, r.client.Patch(ctx, orcImage, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, ssaFieldOwner(SSAFinalizerTxn))
246294
}
247295

248296
// getGlanceImage returns the glance image associated with an ORC Image, or nil if none was found.
@@ -251,12 +299,6 @@ func (r *orcImageReconciler) reconcileDelete(ctx context.Context, orcImage *orcv
251299
func getGlanceImage(ctx context.Context, orcImage *orcv1alpha1.Image, imageClient clients.ImageClient) (*images.Image, error) {
252300
log := ctrl.LoggerFrom(ctx)
253301

254-
if orcImage.Status.ImageID != nil {
255-
log.V(4).Info("Fetching existing glance image", "imageID", *orcImage.Status.ImageID)
256-
257-
return imageClient.GetImage(*orcImage.Status.ImageID)
258-
}
259-
260302
log.V(4).Info("Looking for existing glance image to adopt")
261303

262304
// Check for existing image by name in case we're adopting or failed to write to status
@@ -279,10 +321,38 @@ func getGlanceImage(ctx context.Context, orcImage *orcv1alpha1.Image, imageClien
279321

280322
// getImageName returns the name of the glance image we should use.
281323
func getImageName(orcImage *orcv1alpha1.Image) string {
282-
if orcImage.Spec.ImageName != nil {
283-
return *orcImage.Spec.ImageName
324+
return ptr.Deref(orcImage.Spec.Resource.Name, orcImage.Name)
325+
}
326+
327+
func listOptsFromImportFilter(filter *orcv1alpha1.ImageFilter) images.ListOptsBuilder {
328+
return images.ListOpts{Name: ptr.Deref(filter.Name, "")}
329+
}
330+
331+
// listOptsFromCreation returns a listOpts which will return the image which
332+
// would have been created from the current spec and hopefully no other image.
333+
// Its purpose is to automatically adopt an image that we created but failed to
334+
// write to status.id.
335+
func listOptsFromCreation(orcImage *orcv1alpha1.Image) images.ListOptsBuilder {
336+
return images.ListOpts{Name: getImageName(orcImage)}
337+
}
338+
339+
func getGlanceImageFromList(_ context.Context, listOpts images.ListOptsBuilder, imageClient clients.ImageClient) (*images.Image, error) {
340+
glanceImages, err := imageClient.ListImages(listOpts)
341+
if err != nil {
342+
return nil, err
284343
}
285-
return orcImage.Name
344+
345+
if len(glanceImages) == 1 {
346+
return &glanceImages[0], nil
347+
}
348+
349+
// No image found
350+
if len(glanceImages) == 0 {
351+
return nil, nil
352+
}
353+
354+
// Multiple images found
355+
return nil, capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, fmt.Sprintf("Expected to find exactly one image to import. Found %d", len(glanceImages)))
286356
}
287357

288358
// glancePropertiesFromStruct populates a properties struct using field values and glance tags defined on the given struct
@@ -306,7 +376,7 @@ func glancePropertiesFromStruct(propStruct interface{}, properties map[string]st
306376
field := st.Field(i)
307377
glanceTag, ok := field.Tag.Lookup(orcv1alpha1.GlanceTag)
308378
if !ok {
309-
return fmt.Errorf("glance tag not defined for field %s on struct %T", field.Name, st.Name)
379+
panic(fmt.Errorf("glance tag not defined for field %s on struct %T", field.Name, st.Name))
310380
}
311381

312382
value := s.Field(i)
@@ -328,23 +398,35 @@ func glancePropertiesFromStruct(propStruct interface{}, properties map[string]st
328398

329399
// createImage creates a glance image for an ORC Image.
330400
func createImage(ctx context.Context, orcImage *orcv1alpha1.Image, imageClient clients.ImageClient) (*images.Image, error) {
401+
if orcImage.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyUnmanaged {
402+
// Should have been caught by API validation
403+
return nil, capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Not creating unmanaged resource")
404+
}
405+
331406
log := ctrl.LoggerFrom(ctx)
332407
log.V(3).Info("Creating image")
333408

334-
if orcImage.Spec.Content == nil {
409+
resource := orcImage.Spec.Resource
410+
411+
if resource == nil {
412+
// Should have been caught by API validation
413+
return nil, capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set")
414+
}
415+
416+
if resource.Content == nil {
335417
// Should have been caught by API validation
336-
return nil, capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Creation requested, but spec.content is not set")
418+
return nil, capoerrors.Terminal(orcv1alpha1.OpenStackConditionReasonInvalidConfiguration, "Creation requested, but spec.resource.content is not set")
337419
}
338420

339-
tags := make([]string, len(orcImage.Spec.Tags))
340-
for i := range orcImage.Spec.Tags {
341-
tags[i] = string(orcImage.Spec.Tags[i])
421+
tags := make([]string, len(resource.Tags))
422+
for i := range resource.Tags {
423+
tags[i] = string(resource.Tags[i])
342424
}
343425
// Sort tags before creation to simplify comparisons
344426
slices.Sort(tags)
345427

346428
var minDisk, minMemory int
347-
properties := orcImage.Spec.Properties
429+
properties := resource.Properties
348430
additionalProperties := map[string]string{}
349431
if properties != nil {
350432
if properties.MinDiskGB != nil {
@@ -360,19 +442,19 @@ func createImage(ctx context.Context, orcImage *orcv1alpha1.Image, imageClient c
360442
}
361443

362444
var visibility *images.ImageVisibility
363-
if orcImage.Spec.Visibility != nil {
364-
visibility = ptr.To(images.ImageVisibility(*orcImage.Spec.Visibility))
445+
if resource.Visibility != nil {
446+
visibility = ptr.To(images.ImageVisibility(*resource.Visibility))
365447
}
366448

367449
image, err := imageClient.CreateImage(ctx, &images.CreateOpts{
368450
Name: getImageName(orcImage),
369451
Visibility: visibility,
370452
Tags: tags,
371-
ContainerFormat: string(orcImage.Spec.Content.ContainerFormat),
372-
DiskFormat: (string)(orcImage.Spec.Content.DiskFormat),
453+
ContainerFormat: string(resource.Content.ContainerFormat),
454+
DiskFormat: (string)(resource.Content.DiskFormat),
373455
MinDisk: minDisk,
374456
MinRAM: minMemory,
375-
Protected: orcImage.Spec.Protected,
457+
Protected: resource.Protected,
376458
Properties: additionalProperties,
377459
})
378460

0 commit comments

Comments
 (0)