Skip to content

Commit f4948c6

Browse files
committed
Implement VolumeSnapshot IRI methods in volume poollet & broker
1 parent 311a254 commit f4948c6

23 files changed

+2890
-51
lines changed

api/storage/v1alpha1/common.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ package v1alpha1
66
import corev1 "k8s.io/api/core/v1"
77

88
const (
9-
VolumeVolumePoolRefNameField = "spec.volumePoolRef.name"
10-
VolumeVolumeClassRefNameField = "spec.volumeClassRef.name"
9+
VolumeVolumePoolRefNameField = "spec.volumePoolRef.name"
10+
VolumeVolumeClassRefNameField = "spec.volumeClassRef.name"
11+
VolumeVolumeSnapshotRefNameField = "spec.volumeDataSource.volumeSnapshotRef.name"
1112

1213
BucketBucketPoolRefNameField = "spec.bucketPoolRef.name"
1314
BucketBucketClassRefNameField = "spec.bucketClassRef.name"

broker/volumebroker/server/server_suite_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ func SetupTest() (*corev1.Namespace, *server.Server) {
142142

143143
newSrv, err := server.New(cfg, server.Options{
144144
BrokerDownwardAPILabels: map[string]string{
145-
"root-volume-uid": volumepoolletv1alpha1.VolumeUIDLabel,
145+
"root-volume-uid": volumepoolletv1alpha1.VolumeUIDLabel,
146+
"root-volume-snapshot-uid": volumepoolletv1alpha1.VolumeSnapshotUIDLabel,
146147
},
147148
Namespace: ns.Name,
148149
VolumePoolName: volumePool.Name,

broker/volumebroker/server/volume_create.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
volumepoolletv1alpha1 "github.com/ironcore-dev/ironcore/poollet/volumepoollet/api/v1alpha1"
1919

2020
utilsmaps "github.com/ironcore-dev/ironcore/utils/maps"
21+
"google.golang.org/grpc/codes"
22+
"google.golang.org/grpc/status"
2123
corev1 "k8s.io/api/core/v1"
2224
"k8s.io/apimachinery/pkg/api/resource"
2325
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,7 +32,7 @@ type AggregateIronCoreVolume struct {
3032
AccessSecret *corev1.Secret
3133
}
3234

33-
func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume) (*AggregateIronCoreVolume, error) {
35+
func (s *Server) getIronCoreVolumeConfig(ctx context.Context, volume *iri.Volume) (*AggregateIronCoreVolume, error) {
3436
var volumePoolRef *corev1.LocalObjectReference
3537
if s.volumePoolName != "" {
3638
volumePoolRef = &corev1.LocalObjectReference{
@@ -66,6 +68,34 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
6668
volumepoolletv1alpha1.VolumeDownwardAPIPrefix,
6769
)
6870

71+
var image string
72+
image = volume.Spec.Image
73+
if dataSource := volume.Spec.VolumeDataSource; dataSource != nil && dataSource.ImageDataSource != nil {
74+
image = dataSource.ImageDataSource.Image
75+
}
76+
77+
var volumeSnapshotRef *corev1.LocalObjectReference
78+
if dataSource := volume.Spec.VolumeDataSource; dataSource != nil && dataSource.SnapshotDataSource != nil {
79+
volumeSnapshot, err := s.findVolumeSnapshotBySnapshotID(ctx, dataSource.SnapshotDataSource.SnapshotId)
80+
if err != nil {
81+
return nil, fmt.Errorf("error finding volume snapshot by snapshot ID %s: %w", dataSource.SnapshotDataSource.SnapshotId, err)
82+
}
83+
if volumeSnapshot == nil {
84+
return nil, status.Errorf(codes.NotFound, "volume snapshot with ID %s not found", dataSource.SnapshotDataSource.SnapshotId)
85+
}
86+
if volumeSnapshot.Status.State != storagev1alpha1.VolumeSnapshotStateReady {
87+
switch volumeSnapshot.Status.State {
88+
case storagev1alpha1.VolumeSnapshotStatePending:
89+
return nil, status.Errorf(codes.FailedPrecondition, "volume snapshot %s is not ready (state: %s)", volumeSnapshot.Name, volumeSnapshot.Status.State)
90+
case storagev1alpha1.VolumeSnapshotStateFailed:
91+
return nil, status.Errorf(codes.Internal, "volume snapshot %s has failed (state: %s)", volumeSnapshot.Name, volumeSnapshot.Status.State)
92+
default:
93+
return nil, status.Errorf(codes.FailedPrecondition, "volume snapshot %s is not ready (state: %s)", volumeSnapshot.Name, volumeSnapshot.Status.State)
94+
}
95+
}
96+
volumeSnapshotRef = &corev1.LocalObjectReference{Name: volumeSnapshot.Name}
97+
}
98+
6999
ironcoreVolume := &storagev1alpha1.Volume{
70100
ObjectMeta: metav1.ObjectMeta{
71101
Namespace: s.namespace,
@@ -81,9 +111,13 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
81111
Resources: corev1alpha1.ResourceList{
82112
corev1alpha1.ResourceStorage: *resource.NewQuantity(volume.Spec.Resources.StorageBytes, resource.DecimalSI),
83113
},
84-
Image: volume.Spec.Image,
85-
ImagePullSecretRef: nil, // TODO: Fill if necessary
114+
Image: image, // TODO: Remove this once the image field is deprecated
115+
ImagePullSecretRef: nil, // TODO: Fill if necessary
86116
Encryption: encryption,
117+
VolumeDataSource: storagev1alpha1.VolumeDataSource{
118+
VolumeSnapshotRef: volumeSnapshotRef,
119+
OSImage: getOSImageIfPresent(image),
120+
},
87121
},
88122
}
89123
if err := apiutils.SetObjectMetadata(ironcoreVolume, volume.Metadata); err != nil {
@@ -96,6 +130,28 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
96130
}, nil
97131
}
98132

133+
func getOSImageIfPresent(image string) *string {
134+
if image == "" {
135+
return nil
136+
}
137+
return &image
138+
}
139+
140+
func (s *Server) findVolumeSnapshotBySnapshotID(ctx context.Context, snapshotID string) (*storagev1alpha1.VolumeSnapshot, error) {
141+
volumeSnapshotList := &storagev1alpha1.VolumeSnapshotList{}
142+
if err := s.client.List(ctx, volumeSnapshotList, client.InNamespace(s.namespace)); err != nil {
143+
return nil, fmt.Errorf("error listing volume snapshots: %w", err)
144+
}
145+
146+
for _, volumeSnapshot := range volumeSnapshotList.Items {
147+
if volumeSnapshot.Status.SnapshotID == snapshotID {
148+
return &volumeSnapshot, nil
149+
}
150+
}
151+
152+
return nil, nil
153+
}
154+
99155
func (s *Server) createIronCoreVolume(ctx context.Context, log logr.Logger, volume *AggregateIronCoreVolume) (retErr error) {
100156
c, cleanup := s.setupCleaner(ctx, log, &retErr)
101157
defer cleanup()

broker/volumebroker/server/volume_create_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414

1515
. "github.com/onsi/ginkgo/v2"
1616
. "github.com/onsi/gomega"
17+
"google.golang.org/grpc/codes"
18+
"google.golang.org/grpc/status"
19+
corev1 "k8s.io/api/core/v1"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1721

1822
"sigs.k8s.io/controller-runtime/pkg/client"
1923
)
@@ -67,4 +71,172 @@ var _ = Describe("CreateVolume", func() {
6771
Expect(ironcoreVolume.Spec.VolumeClassRef.Name).To(Equal(volumeClass.Name))
6872
Expect(ironcoreVolume.Spec.Resources).To(HaveLen(1))
6973
})
74+
75+
It("should correctly create a volume from snapshot", func(ctx SpecContext) {
76+
By("creating a volume snapshot")
77+
volumeSnapshot := &storagev1alpha1.VolumeSnapshot{
78+
ObjectMeta: metav1.ObjectMeta{
79+
Namespace: ns.Name,
80+
Name: "test-snapshot",
81+
},
82+
Spec: storagev1alpha1.VolumeSnapshotSpec{
83+
VolumeRef: &corev1.LocalObjectReference{Name: "source-volume"},
84+
},
85+
Status: storagev1alpha1.VolumeSnapshotStatus{
86+
State: storagev1alpha1.VolumeSnapshotStateReady,
87+
SnapshotID: "test-snapshot-id",
88+
},
89+
}
90+
Expect(k8sClient.Create(ctx, volumeSnapshot)).To(Succeed())
91+
92+
By("creating a volume with snapshot data source")
93+
res, err := srv.CreateVolume(ctx, &iri.CreateVolumeRequest{
94+
Volume: &iri.Volume{
95+
Metadata: &irimeta.ObjectMetadata{
96+
Labels: map[string]string{
97+
volumepoolletv1alpha1.VolumeUIDLabel: "foobar",
98+
},
99+
},
100+
Spec: &iri.VolumeSpec{
101+
Class: volumeClass.Name,
102+
Resources: &iri.VolumeResources{
103+
StorageBytes: 100,
104+
},
105+
VolumeDataSource: &iri.VolumeDataSource{
106+
SnapshotDataSource: &iri.SnapshotDataSource{
107+
SnapshotId: "test-snapshot-id",
108+
},
109+
},
110+
},
111+
},
112+
})
113+
114+
Expect(err).NotTo(HaveOccurred())
115+
Expect(res).NotTo(BeNil())
116+
117+
By("getting the ironcore volume")
118+
ironcoreVolume := &storagev1alpha1.Volume{}
119+
ironcoreVolumeKey := client.ObjectKey{Namespace: ns.Name, Name: res.Volume.Metadata.Id}
120+
Expect(k8sClient.Get(ctx, ironcoreVolumeKey, ironcoreVolume)).To(Succeed())
121+
122+
By("verifying the volume has the correct snapshot reference")
123+
Expect(ironcoreVolume.Spec.VolumeDataSource.VolumeSnapshotRef).NotTo(BeNil())
124+
Expect(ironcoreVolume.Spec.VolumeDataSource.VolumeSnapshotRef.Name).To(Equal("test-snapshot"))
125+
})
126+
127+
It("should return error if snapshot is not found", func(ctx SpecContext) {
128+
By("creating a volume with non-existent snapshot data source")
129+
res, err := srv.CreateVolume(ctx, &iri.CreateVolumeRequest{
130+
Volume: &iri.Volume{
131+
Metadata: &irimeta.ObjectMetadata{
132+
Labels: map[string]string{
133+
volumepoolletv1alpha1.VolumeUIDLabel: "foobar",
134+
},
135+
},
136+
Spec: &iri.VolumeSpec{
137+
Class: volumeClass.Name,
138+
Resources: &iri.VolumeResources{
139+
StorageBytes: 100,
140+
},
141+
VolumeDataSource: &iri.VolumeDataSource{
142+
SnapshotDataSource: &iri.SnapshotDataSource{
143+
SnapshotId: "non-existent-snapshot-id",
144+
},
145+
},
146+
},
147+
},
148+
})
149+
150+
Expect(err).To(HaveOccurred())
151+
Expect(res).To(BeNil())
152+
Expect(status.Code(err)).To(Equal(codes.NotFound))
153+
})
154+
155+
It("should return error if snapshot is in pending state", func(ctx SpecContext) {
156+
By("creating a volume snapshot in pending state")
157+
volumeSnapshot := &storagev1alpha1.VolumeSnapshot{
158+
ObjectMeta: metav1.ObjectMeta{
159+
Namespace: ns.Name,
160+
Name: "pending-snapshot",
161+
},
162+
Spec: storagev1alpha1.VolumeSnapshotSpec{
163+
VolumeRef: &corev1.LocalObjectReference{Name: "source-volume"},
164+
},
165+
Status: storagev1alpha1.VolumeSnapshotStatus{
166+
State: storagev1alpha1.VolumeSnapshotStatePending,
167+
SnapshotID: "pending-snapshot-id",
168+
},
169+
}
170+
Expect(k8sClient.Create(ctx, volumeSnapshot)).To(Succeed())
171+
172+
By("creating a volume with pending snapshot data source")
173+
res, err := srv.CreateVolume(ctx, &iri.CreateVolumeRequest{
174+
Volume: &iri.Volume{
175+
Metadata: &irimeta.ObjectMetadata{
176+
Labels: map[string]string{
177+
volumepoolletv1alpha1.VolumeUIDLabel: "foobar",
178+
},
179+
},
180+
Spec: &iri.VolumeSpec{
181+
Class: volumeClass.Name,
182+
Resources: &iri.VolumeResources{
183+
StorageBytes: 100,
184+
},
185+
VolumeDataSource: &iri.VolumeDataSource{
186+
SnapshotDataSource: &iri.SnapshotDataSource{
187+
SnapshotId: "pending-snapshot-id",
188+
},
189+
},
190+
},
191+
},
192+
})
193+
194+
Expect(err).To(HaveOccurred())
195+
Expect(res).To(BeNil())
196+
Expect(status.Code(err)).To(Equal(codes.FailedPrecondition))
197+
})
198+
199+
It("should return error if snapshot is in failed state", func(ctx SpecContext) {
200+
By("creating a volume snapshot in failed state")
201+
volumeSnapshot := &storagev1alpha1.VolumeSnapshot{
202+
ObjectMeta: metav1.ObjectMeta{
203+
Namespace: ns.Name,
204+
Name: "failed-snapshot",
205+
},
206+
Spec: storagev1alpha1.VolumeSnapshotSpec{
207+
VolumeRef: &corev1.LocalObjectReference{Name: "source-volume"},
208+
},
209+
Status: storagev1alpha1.VolumeSnapshotStatus{
210+
State: storagev1alpha1.VolumeSnapshotStateFailed,
211+
SnapshotID: "failed-snapshot-id",
212+
},
213+
}
214+
Expect(k8sClient.Create(ctx, volumeSnapshot)).To(Succeed())
215+
216+
By("creating a volume with failed snapshot data source")
217+
res, err := srv.CreateVolume(ctx, &iri.CreateVolumeRequest{
218+
Volume: &iri.Volume{
219+
Metadata: &irimeta.ObjectMetadata{
220+
Labels: map[string]string{
221+
volumepoolletv1alpha1.VolumeUIDLabel: "foobar",
222+
},
223+
},
224+
Spec: &iri.VolumeSpec{
225+
Class: volumeClass.Name,
226+
Resources: &iri.VolumeResources{
227+
StorageBytes: 100,
228+
},
229+
VolumeDataSource: &iri.VolumeDataSource{
230+
SnapshotDataSource: &iri.SnapshotDataSource{
231+
SnapshotId: "failed-snapshot-id",
232+
},
233+
},
234+
},
235+
},
236+
})
237+
238+
Expect(err).To(HaveOccurred())
239+
Expect(res).To(BeNil())
240+
Expect(status.Code(err)).To(Equal(codes.Internal))
241+
})
70242
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package server
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
11+
"github.com/ironcore-dev/ironcore/broker/volumebroker/apiutils"
12+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
func (s *Server) convertIronCoreVolumeSnapshot(ctx context.Context, ironcoreVolumeSnapshot *storagev1alpha1.VolumeSnapshot) (*iri.VolumeSnapshot, error) {
17+
metadata, err := apiutils.GetObjectMetadata(ironcoreVolumeSnapshot)
18+
if err != nil {
19+
return nil, fmt.Errorf("error getting object metadata: %w", err)
20+
}
21+
22+
var volumeID string
23+
if ironcoreVolumeSnapshot.Spec.VolumeRef != nil {
24+
volume := &storagev1alpha1.Volume{}
25+
if err := s.client.Get(ctx, client.ObjectKey{Namespace: s.namespace, Name: ironcoreVolumeSnapshot.Spec.VolumeRef.Name}, volume); err != nil {
26+
return nil, fmt.Errorf("error getting referenced volume %s for volume snapshot: %w", ironcoreVolumeSnapshot.Spec.VolumeRef.Name, err)
27+
}
28+
volumeID = volume.Status.VolumeID
29+
}
30+
31+
state, err := s.convertIronCoreVolumeSnapshotState(ironcoreVolumeSnapshot.Status.State)
32+
if err != nil {
33+
return nil, fmt.Errorf("error converting volume snapshot state: %w", err)
34+
}
35+
36+
iriVolumeSnapshot := &iri.VolumeSnapshot{
37+
Metadata: metadata,
38+
Spec: &iri.VolumeSnapshotSpec{
39+
VolumeId: volumeID,
40+
},
41+
Status: &iri.VolumeSnapshotStatus{
42+
State: state,
43+
},
44+
}
45+
46+
if ironcoreVolumeSnapshot.Status.Size != nil {
47+
iriVolumeSnapshot.Status.Size = ironcoreVolumeSnapshot.Status.Size.Value()
48+
}
49+
50+
return iriVolumeSnapshot, nil
51+
}
52+
53+
var ironcoreVolumeSnapshotStateToIRIState = map[storagev1alpha1.VolumeSnapshotState]iri.VolumeSnapshotState{
54+
storagev1alpha1.VolumeSnapshotStatePending: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_PENDING,
55+
storagev1alpha1.VolumeSnapshotStateReady: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_READY,
56+
storagev1alpha1.VolumeSnapshotStateFailed: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_FAILED,
57+
}
58+
59+
func (s *Server) convertIronCoreVolumeSnapshotState(state storagev1alpha1.VolumeSnapshotState) (iri.VolumeSnapshotState, error) {
60+
if state, ok := ironcoreVolumeSnapshotStateToIRIState[state]; ok {
61+
return state, nil
62+
}
63+
return 0, fmt.Errorf("unknown ironcore volume snapshot state %q", state)
64+
}

0 commit comments

Comments
 (0)