Skip to content

Commit acb2523

Browse files
committed
VolumeType: implement API, controller and tests
1 parent 92a62ed commit acb2523

29 files changed

+335
-69
lines changed

api/v1alpha1/volumetype_types.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ type VolumeTypeResourceSpec struct {
2929
// +optional
3030
Description *string `json:"description,omitempty"`
3131

32-
// TODO(scaffolding): Add more types.
33-
// To see what is supported, you can take inspiration from the CreateOpts stucture from
34-
// github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes
35-
//
36-
// Until you have implemented mutability for the field, you must add a CEL validation
37-
// preventing the field being modified:
38-
// `// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="<fieldname> is immutable"`
32+
// extraSpecs is a map of key-value pairs that define extra specifications for the volume type.
33+
// +optional
34+
ExtraSpecs map[string]string `json:"extraSpecs,omitempty"`
35+
36+
// isPublic indicates whether the volume type is public.
37+
// +optional
38+
IsPublic *bool `json:"isPublic,omitempty"`
3939
}
4040

4141
// VolumeTypeFilter defines an existing resource by its properties
@@ -51,9 +51,9 @@ type VolumeTypeFilter struct {
5151
// +optional
5252
Description *string `json:"description,omitempty"`
5353

54-
// TODO(scaffolding): Add more types.
55-
// To see what is supported, you can take inspiration from the ListOpts stucture from
56-
// github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes
54+
// isPublic indicates whether the VolumeType is public.
55+
// +optional
56+
IsPublic *bool `json:"isPublic,omitempty"`
5757
}
5858

5959
// VolumeTypeResourceStatus represents the observed state of the resource.
@@ -68,7 +68,11 @@ type VolumeTypeResourceStatus struct {
6868
// +optional
6969
Description string `json:"description,omitempty"`
7070

71-
// TODO(scaffolding): Add more types.
72-
// To see what is supported, you can take inspiration from the VolumeType stucture from
73-
// github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes
71+
// extraSpecs is a map of key-value pairs that define extra specifications for the volume type.
72+
// +optional
73+
ExtraSpecs map[string]string `json:"extraSpecs"`
74+
75+
// isPublic indicates whether the VolumeType is public.
76+
// +optional
77+
IsPublic *bool `json:"isPublic"`
7478
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/models-schema/zz_generated.openapi.go

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/openstack.k-orc.cloud_volumetypes.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ spec:
9696
maxLength: 255
9797
minLength: 1
9898
type: string
99+
isPublic:
100+
description: isPublic indicates whether the VolumeType is
101+
public.
102+
type: boolean
99103
name:
100104
description: name of the existing resource
101105
maxLength: 255
@@ -155,6 +159,15 @@ spec:
155159
maxLength: 255
156160
minLength: 1
157161
type: string
162+
extraSpecs:
163+
additionalProperties:
164+
type: string
165+
description: extraSpecs is a map of key-value pairs that define
166+
extra specifications for the volume type.
167+
type: object
168+
isPublic:
169+
description: isPublic indicates whether the volume type is public.
170+
type: boolean
158171
name:
159172
description: |-
160173
name will be the name of the created resource. If not specified, the
@@ -271,6 +284,15 @@ spec:
271284
resource.
272285
maxLength: 1024
273286
type: string
287+
extraSpecs:
288+
additionalProperties:
289+
type: string
290+
description: extraSpecs is a map of key-value pairs that define
291+
extra specifications for the volume type.
292+
type: object
293+
isPublic:
294+
description: isPublic indicates whether the VolumeType is public.
295+
type: boolean
274296
name:
275297
description: name is a Human-readable name for the resource. Might
276298
not be unique.

config/samples/openstack_v1alpha1_volumetype.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ metadata:
55
name: volumetype-sample
66
spec:
77
cloudCredentialsRef:
8-
# TODO(scaffolding): Use openstack-admin if the resouce needs admin credentials to be created
9-
cloudName: openstack
8+
cloudName: openstack-admin
109
secretName: openstack-clouds
1110
managementPolicy: managed
1211
resource:
1312
description: Sample VolumeType
14-
# TODO(scaffolding): Add all fields the resource supports
13+
isPublic: false
14+
extraSpecs:
15+
spec1: "foo"
16+
spec2: "bar"

internal/controllers/volumetype/actuator.go

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,30 +70,72 @@ func (actuator volumetypeActuator) ListOSResourcesForAdoption(ctx context.Contex
7070
return nil, false
7171
}
7272

73-
// TODO(scaffolding) If you need to filter resources on fields that the List() function
74-
// of gophercloud does not support, it's possible to perform client-side filtering.
75-
// Check osclients.ResourceFilter
73+
var filters []osclients.ResourceFilter[osResourceT]
74+
75+
// NOTE: The API doesn't allow filtering by name or description, we'll have to do it client-side.
76+
filters = append(filters,
77+
func(f *volumetypes.VolumeType) bool {
78+
name := getResourceName(orcObject)
79+
// Compare non-pointer values
80+
return f.Name == name
81+
},
82+
)
83+
if resourceSpec.Description != nil {
84+
filters = append(filters, func(f *volumetypes.VolumeType) bool {
85+
return f.Description == *resourceSpec.Description
86+
})
87+
}
88+
89+
isPublic := volumetypes.VisibilityDefault
90+
if resourceSpec.IsPublic != nil {
91+
if *resourceSpec.IsPublic {
92+
isPublic = volumetypes.VisibilityPublic
93+
} else {
94+
isPublic = volumetypes.VisibilityPrivate
95+
}
96+
}
7697

7798
listOpts := volumetypes.ListOpts{
78-
Name: getResourceName(orcObject),
79-
Description: ptr.Deref(resourceSpec.Description, ""),
99+
IsPublic: isPublic,
80100
}
81101

82-
return actuator.osClient.ListVolumeTypes(ctx, listOpts), true
102+
return actuator.listOSResources(ctx, filters, listOpts), true
83103
}
84104

85105
func (actuator volumetypeActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
86-
// TODO(scaffolding) If you need to filter resources on fields that the List() function
87-
// of gophercloud does not support, it's possible to perform client-side filtering.
88-
// Check osclients.ResourceFilter
106+
var filters []osclients.ResourceFilter[osResourceT]
107+
108+
// NOTE: The API doesn't allow filtering by name or description, we'll have to do it client-side.
109+
if filter.Name != nil {
110+
filters = append(filters, func(f *volumetypes.VolumeType) bool {
111+
return f.Name == string(*filter.Name)
112+
})
113+
}
114+
if filter.Description != nil {
115+
filters = append(filters, func(f *volumetypes.VolumeType) bool {
116+
return f.Description == *filter.Description
117+
})
118+
}
119+
120+
isPublic := volumetypes.VisibilityDefault
121+
if filter.IsPublic != nil {
122+
if *filter.IsPublic {
123+
isPublic = volumetypes.VisibilityPublic
124+
} else {
125+
isPublic = volumetypes.VisibilityPrivate
126+
}
127+
}
89128

90129
listOpts := volumetypes.ListOpts{
91-
Name: string(ptr.Deref(filter.Name, "")),
92-
Description: string(ptr.Deref(filter.Description, "")),
93-
// TODO(scaffolding): Add more import filters
130+
IsPublic: isPublic,
94131
}
95132

96-
return actuator.osClient.ListVolumeTypes(ctx, listOpts), nil
133+
return actuator.listOSResources(ctx, filters, listOpts), nil
134+
}
135+
136+
func (actuator volumetypeActuator) listOSResources(ctx context.Context, filters []osclients.ResourceFilter[osResourceT], listOpts volumetypes.ListOptsBuilder) iter.Seq2[*volumetypes.VolumeType, error] {
137+
volumetypes := actuator.osClient.ListVolumeTypes(ctx, listOpts)
138+
return osclients.Filter(volumetypes, filters...)
97139
}
98140

99141
func (actuator volumetypeActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) {
@@ -107,7 +149,8 @@ func (actuator volumetypeActuator) CreateResource(ctx context.Context, obj orcOb
107149
createOpts := volumetypes.CreateOpts{
108150
Name: getResourceName(obj),
109151
Description: ptr.Deref(resource.Description, ""),
110-
// TODO(scaffolding): Add more fields
152+
IsPublic: resource.IsPublic,
153+
ExtraSpecs: resource.ExtraSpecs,
111154
}
112155

113156
osResource, err := actuator.osClient.CreateVolumeType(ctx, createOpts)
@@ -139,6 +182,7 @@ func (actuator volumetypeActuator) updateResource(ctx context.Context, obj orcOb
139182

140183
handleNameUpdate(&updateOpts, obj, osResource)
141184
handleDescriptionUpdate(&updateOpts, resource, osResource)
185+
handleIsPublicUpdate(&updateOpts, resource, osResource)
142186

143187
needsUpdate, err := needsUpdate(updateOpts)
144188
if err != nil {
@@ -192,6 +236,14 @@ func handleDescriptionUpdate(updateOpts *volumetypes.UpdateOpts, resource *resou
192236
}
193237
}
194238

239+
func handleIsPublicUpdate(updateOpts *volumetypes.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) {
240+
// Default is true
241+
isPublic := ptr.Deref(resource.IsPublic, true)
242+
if osResource.IsPublic != isPublic {
243+
updateOpts.IsPublic = &isPublic
244+
}
245+
}
246+
195247
func (actuator volumetypeActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) {
196248
return []resourceReconciler{
197249
actuator.updateResource,

internal/controllers/volumetype/actuator_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,30 @@ func TestHandleDescriptionUpdate(t *testing.T) {
117117

118118
}
119119
}
120+
121+
func TestHandleIsPublicUpdate(t *testing.T) {
122+
ptrToBool := ptr.To[bool]
123+
testCases := []struct {
124+
name string
125+
newValue *bool
126+
existingValue bool
127+
expectChange bool
128+
}{
129+
{name: "Identical", newValue: ptrToBool(true), existingValue: true, expectChange: false},
130+
{name: "Different", newValue: ptrToBool(true), existingValue: false, expectChange: true},
131+
{name: "No value provided, existing is set", newValue: nil, existingValue: false, expectChange: true},
132+
{name: "No value provided, existing is default", newValue: nil, existingValue: true, expectChange: false},
133+
}
134+
for _, tt := range testCases {
135+
t.Run(tt.name, func(t *testing.T) {
136+
resource := &orcv1alpha1.VolumeTypeResourceSpec{IsPublic: tt.newValue}
137+
osResource := &volumetypes.VolumeType{IsPublic: tt.existingValue}
138+
updateOpts := volumetypes.UpdateOpts{}
139+
handleIsPublicUpdate(&updateOpts, resource, osResource)
140+
got, _ := needsUpdate(updateOpts)
141+
if got != tt.expectChange {
142+
t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
143+
}
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)