Skip to content

Commit 9a0001a

Browse files
committed
Fix bugs + CI issues
1 parent ce37b4d commit 9a0001a

17 files changed

Lines changed: 469 additions & 89 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ __pycache__/
3333
/openapi.json
3434
/.custom-deploy
3535
/.custom-gcl.yml
36+
.cursor/
3637

3738
# kuttl generates a kubeconfig
3839
/kubeconfig

api/v1alpha1/volumesnapshot_types.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,9 @@ type VolumeSnapshotFilter struct {
6868
// +optional
6969
Status *string `json:"status,omitempty"`
7070

71-
// volumeID is the ID of the volume the snapshot was created from
72-
// +kubebuilder:validation:MinLength:=1
73-
// +kubebuilder:validation:MaxLength:=255
71+
// volumeRef references the ORC Volume used to filter snapshots by source volume.
7472
// +optional
75-
VolumeID *string `json:"volumeID,omitempty"`
73+
VolumeRef *KubernetesNameRef `json:"volumeRef,omitempty"`
7674
}
7775

7876
// VolumeSnapshotResourceStatus represents the observed state of the resource.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 3 additions & 3 deletions
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: 2 additions & 2 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_volumesnapshots.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.17.1
6+
controller-gen.kubebuilder.io/version: v0.20.1
77
name: volumesnapshots.openstack.k-orc.cloud
88
spec:
99
group: openstack.k-orc.cloud
@@ -107,10 +107,10 @@ spec:
107107
maxLength: 255
108108
minLength: 1
109109
type: string
110-
volumeID:
111-
description: volumeID is the ID of the volume the snapshot
112-
was created from
113-
maxLength: 255
110+
volumeRef:
111+
description: volumeRef references the ORC Volume used to filter
112+
snapshots by source volume.
113+
maxLength: 253
114114
minLength: 1
115115
type: string
116116
type: object

internal/controllers/volumesnapshot/actuator.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323

2424
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots"
2525
corev1 "k8s.io/api/core/v1"
26+
apierrors "k8s.io/apimachinery/pkg/api/errors"
27+
"k8s.io/apimachinery/pkg/types"
2628
"k8s.io/utils/ptr"
2729
ctrl "sigs.k8s.io/controller-runtime"
2830
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -100,15 +102,41 @@ func (actuator volumesnapshotActuator) ListOSResourcesForImport(ctx context.Cont
100102
})
101103
}
102104

105+
volumeID, reconcileStatus := actuator.getVolumeIDForImport(ctx, obj, filter)
106+
if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
107+
return nil, reconcileStatus
108+
}
109+
103110
listOpts := snapshots.ListOpts{
104111
Name: string(ptr.Deref(filter.Name, "")),
105112
Status: ptr.Deref(filter.Status, ""),
106-
VolumeID: ptr.Deref(filter.VolumeID, ""),
113+
VolumeID: volumeID,
107114
}
108115

109116
return actuator.listOSResources(ctx, filters, listOpts), nil
110117
}
111118

119+
func (actuator volumesnapshotActuator) getVolumeIDForImport(ctx context.Context, obj orcObjectPT, filter filterT) (string, progress.ReconcileStatus) {
120+
if filter.VolumeRef == nil {
121+
return "", nil
122+
}
123+
124+
volumeName := string(*filter.VolumeRef)
125+
volume := &orcv1alpha1.Volume{}
126+
if err := actuator.k8sClient.Get(ctx, types.NamespacedName{Name: volumeName, Namespace: obj.GetNamespace()}, volume); err != nil {
127+
if apierrors.IsNotFound(err) {
128+
return "", progress.WaitingOnObject("Volume", volumeName, progress.WaitingOnCreation)
129+
}
130+
return "", progress.WrapError(err)
131+
}
132+
133+
if !orcv1alpha1.IsAvailable(volume) || volume.Status.ID == nil || *volume.Status.ID == "" {
134+
return "", progress.WaitingOnObject("Volume", volumeName, progress.WaitingOnReady)
135+
}
136+
137+
return *volume.Status.ID, nil
138+
}
139+
112140
func (actuator volumesnapshotActuator) listOSResources(ctx context.Context, filters []osclients.ResourceFilter[osResourceT], listOpts snapshots.ListOptsBuilder) iter.Seq2[*snapshots.Snapshot, error] {
113141
results := actuator.osClient.ListVolumeSnapshots(ctx, listOpts)
114142
return osclients.Filter(results, filters...)

internal/controllers/volumesnapshot/actuator_test.go

Lines changed: 230 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,60 @@ limitations under the License.
1717
package volumesnapshot
1818

1919
import (
20+
"context"
21+
"errors"
22+
"iter"
23+
"strings"
2024
"testing"
2125

2226
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots"
23-
orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
27+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
2429
"k8s.io/utils/ptr"
30+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
31+
32+
orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
2533
)
2634

35+
var errNotImplemented = errors.New("not implemented")
36+
37+
type testVolumeSnapshotClient struct {
38+
listOpts snapshots.ListOptsBuilder
39+
listCalled bool
40+
}
41+
42+
func (c *testVolumeSnapshotClient) ListVolumeSnapshots(_ context.Context, listOpts snapshots.ListOptsBuilder) iter.Seq2[*snapshots.Snapshot, error] {
43+
c.listCalled = true
44+
c.listOpts = listOpts
45+
return func(yield func(*snapshots.Snapshot, error) bool) {}
46+
}
47+
48+
func (c *testVolumeSnapshotClient) CreateVolumeSnapshot(_ context.Context, _ snapshots.CreateOptsBuilder) (*snapshots.Snapshot, error) {
49+
return nil, errNotImplemented
50+
}
51+
52+
func (c *testVolumeSnapshotClient) DeleteVolumeSnapshot(_ context.Context, _ string) error {
53+
return errNotImplemented
54+
}
55+
56+
func (c *testVolumeSnapshotClient) GetVolumeSnapshot(_ context.Context, _ string) (*snapshots.Snapshot, error) {
57+
return nil, errNotImplemented
58+
}
59+
60+
func (c *testVolumeSnapshotClient) UpdateVolumeSnapshot(_ context.Context, _ string, _ snapshots.UpdateOptsBuilder) (*snapshots.Snapshot, error) {
61+
return nil, errNotImplemented
62+
}
63+
64+
func newTestScheme(t *testing.T) *runtime.Scheme {
65+
t.Helper()
66+
67+
scheme := runtime.NewScheme()
68+
if err := orcv1alpha1.AddToScheme(scheme); err != nil {
69+
t.Fatalf("failed adding API scheme: %v", err)
70+
}
71+
return scheme
72+
}
73+
2774
func TestNeedsUpdate(t *testing.T) {
2875
testCases := []struct {
2976
name string
@@ -44,7 +91,10 @@ func TestNeedsUpdate(t *testing.T) {
4491

4592
for _, tt := range testCases {
4693
t.Run(tt.name, func(t *testing.T) {
47-
got, _ := needsUpdate(tt.updateOpts)
94+
got, err := needsUpdate(tt.updateOpts)
95+
if err != nil {
96+
t.Fatalf("Expected no error, got: %v", err)
97+
}
4898
if got != tt.expectChange {
4999
t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
50100
}
@@ -78,7 +128,10 @@ func TestHandleNameUpdate(t *testing.T) {
78128
updateOpts := snapshots.UpdateOpts{}
79129
handleNameUpdate(&updateOpts, resource, osResource)
80130

81-
got, _ := needsUpdate(updateOpts)
131+
got, err := needsUpdate(updateOpts)
132+
if err != nil {
133+
t.Fatalf("Expected no error, got: %v", err)
134+
}
82135
if got != tt.expectChange {
83136
t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
84137
}
@@ -109,11 +162,184 @@ func TestHandleDescriptionUpdate(t *testing.T) {
109162
updateOpts := snapshots.UpdateOpts{}
110163
handleDescriptionUpdate(&updateOpts, resource, osResource)
111164

112-
got, _ := needsUpdate(updateOpts)
165+
got, err := needsUpdate(updateOpts)
166+
if err != nil {
167+
t.Fatalf("Expected no error, got: %v", err)
168+
}
113169
if got != tt.expectChange {
114170
t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
115171
}
116172
})
117173

118174
}
119175
}
176+
177+
func TestGetVolumeIDForImport(t *testing.T) {
178+
ctx := context.Background()
179+
namespace := "default"
180+
volumeRef := orcv1alpha1.KubernetesNameRef("import-volume")
181+
orcObject := &orcv1alpha1.VolumeSnapshot{
182+
ObjectMeta: v1.ObjectMeta{
183+
Name: "import-snapshot",
184+
Namespace: namespace,
185+
},
186+
}
187+
188+
t.Run("waits for missing volume", func(t *testing.T) {
189+
actuator := volumesnapshotActuator{
190+
k8sClient: fake.NewClientBuilder().
191+
WithScheme(newTestScheme(t)).
192+
Build(),
193+
}
194+
195+
volumeID, reconcileStatus := actuator.getVolumeIDForImport(ctx, orcObject, orcv1alpha1.VolumeSnapshotFilter{
196+
VolumeRef: &volumeRef,
197+
})
198+
if volumeID != "" {
199+
t.Fatalf("expected empty volume ID, got %q", volumeID)
200+
}
201+
if reconcileStatus == nil {
202+
t.Fatalf("expected reconcile status, got nil")
203+
}
204+
needsReschedule, err := reconcileStatus.NeedsReschedule()
205+
if err != nil {
206+
t.Fatalf("expected no error, got %v", err)
207+
}
208+
if !needsReschedule {
209+
t.Fatalf("expected reconcile status to require reschedule")
210+
}
211+
if !strings.Contains(strings.Join(reconcileStatus.GetProgressMessages(), "\n"), "Waiting for Volume/import-volume to be created") {
212+
t.Fatalf("expected waiting for volume creation message, got %v", reconcileStatus.GetProgressMessages())
213+
}
214+
})
215+
216+
t.Run("waits for not-ready volume", func(t *testing.T) {
217+
volume := &orcv1alpha1.Volume{
218+
ObjectMeta: v1.ObjectMeta{
219+
Name: "import-volume",
220+
Namespace: namespace,
221+
},
222+
}
223+
224+
actuator := volumesnapshotActuator{
225+
k8sClient: fake.NewClientBuilder().
226+
WithScheme(newTestScheme(t)).
227+
WithObjects(volume).
228+
Build(),
229+
}
230+
231+
volumeID, reconcileStatus := actuator.getVolumeIDForImport(ctx, orcObject, orcv1alpha1.VolumeSnapshotFilter{
232+
VolumeRef: &volumeRef,
233+
})
234+
if volumeID != "" {
235+
t.Fatalf("expected empty volume ID, got %q", volumeID)
236+
}
237+
if reconcileStatus == nil {
238+
t.Fatalf("expected reconcile status, got nil")
239+
}
240+
needsReschedule, err := reconcileStatus.NeedsReschedule()
241+
if err != nil {
242+
t.Fatalf("expected no error, got %v", err)
243+
}
244+
if !needsReschedule {
245+
t.Fatalf("expected reconcile status to require reschedule")
246+
}
247+
if !strings.Contains(strings.Join(reconcileStatus.GetProgressMessages(), "\n"), "Waiting for Volume/import-volume to be ready") {
248+
t.Fatalf("expected waiting for volume ready message, got %v", reconcileStatus.GetProgressMessages())
249+
}
250+
})
251+
252+
t.Run("returns volume ID when volume is ready", func(t *testing.T) {
253+
volume := &orcv1alpha1.Volume{
254+
ObjectMeta: v1.ObjectMeta{
255+
Name: "import-volume",
256+
Namespace: namespace,
257+
},
258+
Status: orcv1alpha1.VolumeStatus{
259+
ID: ptr.To("volume-id"),
260+
Conditions: []v1.Condition{
261+
{
262+
Type: orcv1alpha1.ConditionAvailable,
263+
Status: v1.ConditionTrue,
264+
},
265+
},
266+
},
267+
}
268+
269+
actuator := volumesnapshotActuator{
270+
k8sClient: fake.NewClientBuilder().
271+
WithScheme(newTestScheme(t)).
272+
WithObjects(volume).
273+
Build(),
274+
}
275+
276+
volumeID, reconcileStatus := actuator.getVolumeIDForImport(ctx, orcObject, orcv1alpha1.VolumeSnapshotFilter{
277+
VolumeRef: &volumeRef,
278+
})
279+
if reconcileStatus != nil {
280+
t.Fatalf("expected nil reconcile status, got %v", reconcileStatus)
281+
}
282+
if volumeID != "volume-id" {
283+
t.Fatalf("expected volume-id, got %q", volumeID)
284+
}
285+
})
286+
}
287+
288+
func TestListOSResourcesForImport_ResolvesVolumeRefToVolumeID(t *testing.T) {
289+
ctx := context.Background()
290+
namespace := "default"
291+
volumeRef := orcv1alpha1.KubernetesNameRef("import-volume")
292+
293+
volume := &orcv1alpha1.Volume{
294+
ObjectMeta: v1.ObjectMeta{
295+
Name: "import-volume",
296+
Namespace: namespace,
297+
},
298+
Status: orcv1alpha1.VolumeStatus{
299+
ID: ptr.To("volume-id"),
300+
Conditions: []v1.Condition{
301+
{
302+
Type: orcv1alpha1.ConditionAvailable,
303+
Status: v1.ConditionTrue,
304+
},
305+
},
306+
},
307+
}
308+
309+
osClient := &testVolumeSnapshotClient{}
310+
actuator := volumesnapshotActuator{
311+
osClient: osClient,
312+
k8sClient: fake.NewClientBuilder().
313+
WithScheme(newTestScheme(t)).
314+
WithObjects(volume).
315+
Build(),
316+
}
317+
318+
orcObject := &orcv1alpha1.VolumeSnapshot{
319+
ObjectMeta: v1.ObjectMeta{
320+
Name: "import-snapshot",
321+
Namespace: namespace,
322+
},
323+
}
324+
325+
filter := orcv1alpha1.VolumeSnapshotFilter{
326+
Name: ptr.To(orcv1alpha1.OpenStackName("snapshot-name")),
327+
VolumeRef: &volumeRef,
328+
}
329+
330+
_, reconcileStatus := actuator.ListOSResourcesForImport(ctx, orcObject, filter)
331+
if reconcileStatus != nil {
332+
t.Fatalf("expected nil reconcile status, got %v", reconcileStatus)
333+
}
334+
if !osClient.listCalled {
335+
t.Fatalf("expected ListVolumeSnapshots to be called")
336+
}
337+
338+
listOpts, ok := osClient.listOpts.(snapshots.ListOpts)
339+
if !ok {
340+
t.Fatalf("expected snapshots.ListOpts, got %T", osClient.listOpts)
341+
}
342+
if listOpts.VolumeID != "volume-id" {
343+
t.Fatalf("expected ListOpts.VolumeID to be volume-id, got %q", listOpts.VolumeID)
344+
}
345+
}

0 commit comments

Comments
 (0)