Skip to content

Commit 4cc5e6f

Browse files
authored
Implement VolumeSnapshot IRI methods in volume poollet & broker (#1388)
1 parent 67e4ae3 commit 4cc5e6f

23 files changed

+2389
-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.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: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
6666
volumepoolletv1alpha1.VolumeDownwardAPIPrefix,
6767
)
6868

69+
var image string
70+
var volumeSnapshotRef *corev1.LocalObjectReference
71+
72+
image = volume.Spec.Image // TODO: Remove this once volume.Spec.Image is deprecated
73+
74+
if dataSource := volume.Spec.VolumeDataSource; dataSource != nil {
75+
switch {
76+
case dataSource.SnapshotDataSource != nil:
77+
volumeSnapshotRef = &corev1.LocalObjectReference{Name: dataSource.SnapshotDataSource.SnapshotId}
78+
image = "" // TODO: Remove this once volume.Spec.Image is deprecated
79+
case dataSource.ImageDataSource != nil:
80+
image = dataSource.ImageDataSource.Image
81+
}
82+
}
83+
6984
ironcoreVolume := &storagev1alpha1.Volume{
7085
ObjectMeta: metav1.ObjectMeta{
7186
Namespace: s.namespace,
@@ -81,9 +96,18 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
8196
Resources: corev1alpha1.ResourceList{
8297
corev1alpha1.ResourceStorage: *resource.NewQuantity(volume.Spec.Resources.StorageBytes, resource.DecimalSI),
8398
},
84-
Image: volume.Spec.Image,
85-
ImagePullSecretRef: nil, // TODO: Fill if necessary
99+
Image: image, // TODO: Remove this once volume.Spec.Image is deprecated
100+
ImagePullSecretRef: nil, // TODO: Fill if necessary
86101
Encryption: encryption,
102+
VolumeDataSource: storagev1alpha1.VolumeDataSource{
103+
VolumeSnapshotRef: volumeSnapshotRef,
104+
OSImage: func() *string {
105+
if image == "" {
106+
return nil
107+
}
108+
return &image
109+
}(),
110+
},
87111
},
88112
}
89113
if err := apiutils.SetObjectMetadata(ironcoreVolume, volume.Metadata); err != nil {

broker/volumebroker/server/volume_create_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import (
1414

1515
. "github.com/onsi/ginkgo/v2"
1616
. "github.com/onsi/gomega"
17-
17+
corev1 "k8s.io/api/core/v1"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1819
"sigs.k8s.io/controller-runtime/pkg/client"
1920
)
2021

@@ -67,4 +68,57 @@ var _ = Describe("CreateVolume", func() {
6768
Expect(ironcoreVolume.Spec.VolumeClassRef.Name).To(Equal(volumeClass.Name))
6869
Expect(ironcoreVolume.Spec.Resources).To(HaveLen(1))
6970
})
71+
72+
It("should correctly create a volume from snapshot", func(ctx SpecContext) {
73+
By("creating a volume snapshot")
74+
volumeSnapshot := &storagev1alpha1.VolumeSnapshot{
75+
ObjectMeta: metav1.ObjectMeta{
76+
Namespace: ns.Name,
77+
Name: "test-snapshot",
78+
},
79+
Spec: storagev1alpha1.VolumeSnapshotSpec{
80+
VolumeRef: &corev1.LocalObjectReference{Name: "source-volume"},
81+
},
82+
Status: storagev1alpha1.VolumeSnapshotStatus{
83+
State: storagev1alpha1.VolumeSnapshotStateReady,
84+
SnapshotID: "test-snapshot-id",
85+
},
86+
}
87+
Expect(k8sClient.Create(ctx, volumeSnapshot)).To(Succeed())
88+
89+
By("creating a volume with snapshot data source")
90+
res, err := srv.CreateVolume(ctx, &iri.CreateVolumeRequest{
91+
Volume: &iri.Volume{
92+
Metadata: &irimeta.ObjectMetadata{
93+
Labels: map[string]string{
94+
volumepoolletv1alpha1.VolumeUIDLabel: "foobar",
95+
},
96+
},
97+
Spec: &iri.VolumeSpec{
98+
Class: volumeClass.Name,
99+
Resources: &iri.VolumeResources{
100+
StorageBytes: 100,
101+
},
102+
VolumeDataSource: &iri.VolumeDataSource{
103+
SnapshotDataSource: &iri.SnapshotDataSource{
104+
SnapshotId: "test-snapshot",
105+
},
106+
},
107+
},
108+
},
109+
})
110+
111+
Expect(err).NotTo(HaveOccurred())
112+
Expect(res).NotTo(BeNil())
113+
114+
By("getting the ironcore volume")
115+
ironcoreVolume := &storagev1alpha1.Volume{}
116+
ironcoreVolumeKey := client.ObjectKey{Namespace: ns.Name, Name: res.Volume.Metadata.Id}
117+
Expect(k8sClient.Get(ctx, ironcoreVolumeKey, ironcoreVolume)).To(Succeed())
118+
119+
By("verifying the volume has the correct snapshot reference")
120+
Expect(ironcoreVolume.Spec.VolumeDataSource.VolumeSnapshotRef).NotTo(BeNil())
121+
Expect(ironcoreVolume.Spec.VolumeDataSource.VolumeSnapshotRef.Name).To(Equal("test-snapshot"))
122+
})
123+
70124
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
"fmt"
8+
9+
storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
10+
"github.com/ironcore-dev/ironcore/broker/volumebroker/apiutils"
11+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
12+
)
13+
14+
func (s *Server) convertIronCoreVolumeSnapshot(ironcoreVolumeSnapshot *storagev1alpha1.VolumeSnapshot) (*iri.VolumeSnapshot, error) {
15+
metadata, err := apiutils.GetObjectMetadata(ironcoreVolumeSnapshot)
16+
if err != nil {
17+
return nil, fmt.Errorf("error getting object metadata: %w", err)
18+
}
19+
20+
state, err := s.convertIronCoreVolumeSnapshotState(ironcoreVolumeSnapshot.Status.State)
21+
if err != nil {
22+
return nil, fmt.Errorf("error converting volume snapshot state: %w", err)
23+
}
24+
25+
iriVolumeSnapshot := &iri.VolumeSnapshot{
26+
Metadata: metadata,
27+
Status: &iri.VolumeSnapshotStatus{
28+
State: state,
29+
},
30+
}
31+
32+
if ironcoreVolumeSnapshot.Status.Size != nil {
33+
iriVolumeSnapshot.Status.Size = ironcoreVolumeSnapshot.Status.Size.Value()
34+
}
35+
36+
return iriVolumeSnapshot, nil
37+
}
38+
39+
var ironcoreVolumeSnapshotStateToIRIState = map[storagev1alpha1.VolumeSnapshotState]iri.VolumeSnapshotState{
40+
storagev1alpha1.VolumeSnapshotStatePending: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_PENDING,
41+
storagev1alpha1.VolumeSnapshotStateReady: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_READY,
42+
storagev1alpha1.VolumeSnapshotStateFailed: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_FAILED,
43+
}
44+
45+
func (s *Server) convertIronCoreVolumeSnapshotState(state storagev1alpha1.VolumeSnapshotState) (iri.VolumeSnapshotState, error) {
46+
if state, ok := ironcoreVolumeSnapshotStateToIRIState[state]; ok {
47+
return state, nil
48+
}
49+
return 0, fmt.Errorf("unknown ironcore volume snapshot state %q", state)
50+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
"github.com/go-logr/logr"
11+
storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
12+
brokerutils "github.com/ironcore-dev/ironcore/broker/common/utils"
13+
volumebrokerv1alpha1 "github.com/ironcore-dev/ironcore/broker/volumebroker/api/v1alpha1"
14+
"github.com/ironcore-dev/ironcore/broker/volumebroker/apiutils"
15+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
16+
volumepoolletv1alpha1 "github.com/ironcore-dev/ironcore/poollet/volumepoollet/api/v1alpha1"
17+
utilsmaps "github.com/ironcore-dev/ironcore/utils/maps"
18+
corev1 "k8s.io/api/core/v1"
19+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
21+
)
22+
23+
func (s *Server) getIronCoreVolumeSnapshotConfig(volumeSnapshot *iri.VolumeSnapshot) (*storagev1alpha1.VolumeSnapshot, error) {
24+
labels := brokerutils.PrepareDownwardAPILabels(
25+
volumeSnapshot.Metadata.Labels,
26+
s.brokerDownwardAPILabels,
27+
volumepoolletv1alpha1.VolumeSnapshotDownwardAPIPrefix,
28+
)
29+
30+
ironcoreVolumeSnapshot := &storagev1alpha1.VolumeSnapshot{
31+
ObjectMeta: metav1.ObjectMeta{
32+
Namespace: s.namespace,
33+
Name: s.idGen.Generate(),
34+
Labels: utilsmaps.AppendMap(labels, map[string]string{
35+
volumebrokerv1alpha1.ManagerLabel: volumebrokerv1alpha1.VolumeBrokerManager,
36+
}),
37+
Annotations: volumeSnapshot.Metadata.Annotations,
38+
},
39+
Spec: storagev1alpha1.VolumeSnapshotSpec{
40+
VolumeRef: &corev1.LocalObjectReference{
41+
Name: volumeSnapshot.Spec.VolumeId,
42+
},
43+
},
44+
}
45+
46+
if err := apiutils.SetObjectMetadata(ironcoreVolumeSnapshot, volumeSnapshot.Metadata); err != nil {
47+
return nil, err
48+
}
49+
50+
return ironcoreVolumeSnapshot, nil
51+
}
52+
53+
func (s *Server) createIronCoreVolumeSnapshot(ctx context.Context, log logr.Logger, volumeSnapshot *storagev1alpha1.VolumeSnapshot) (retErr error) {
54+
c, cleanup := s.setupCleaner(ctx, log, &retErr)
55+
defer cleanup()
56+
57+
log.V(1).Info("Creating ironcore volume snapshot")
58+
if err := s.client.Create(ctx, volumeSnapshot); err != nil {
59+
return fmt.Errorf("error creating ironcore volume snapshot: %w", err)
60+
}
61+
c.Add(func(ctx context.Context) error {
62+
if err := s.client.Delete(ctx, volumeSnapshot); client.IgnoreNotFound(err) != nil {
63+
return fmt.Errorf("error deleting ironcore volume snapshot: %w", err)
64+
}
65+
return nil
66+
})
67+
68+
log.V(1).Info("Patching ironcore volume snapshot as created")
69+
if err := apiutils.PatchCreated(ctx, s.client, volumeSnapshot); err != nil {
70+
return fmt.Errorf("error patching ironcore volume snapshot as created: %w", err)
71+
}
72+
73+
// Reset cleaner since everything from now on operates on a consistent volume snapshot
74+
c.Reset()
75+
76+
return nil
77+
}
78+
79+
func (s *Server) CreateVolumeSnapshot(ctx context.Context, req *iri.CreateVolumeSnapshotRequest) (res *iri.CreateVolumeSnapshotResponse, retErr error) {
80+
log := s.loggerFrom(ctx)
81+
82+
log.V(1).Info("Getting volume snapshot configuration")
83+
ironcoreVolumeSnapshot, err := s.getIronCoreVolumeSnapshotConfig(req.VolumeSnapshot)
84+
if err != nil {
85+
return nil, fmt.Errorf("error getting ironcore volume snapshot config: %w", err)
86+
}
87+
88+
if err := s.createIronCoreVolumeSnapshot(ctx, log, ironcoreVolumeSnapshot); err != nil {
89+
return nil, fmt.Errorf("error creating ironcore volume snapshot: %w", err)
90+
}
91+
92+
v, err := s.convertIronCoreVolumeSnapshot(ironcoreVolumeSnapshot)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
return &iri.CreateVolumeSnapshotResponse{
98+
VolumeSnapshot: v,
99+
}, nil
100+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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_test
5+
6+
import (
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
10+
corev1alpha1 "github.com/ironcore-dev/ironcore/api/core/v1alpha1"
11+
storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
12+
"github.com/ironcore-dev/ironcore/broker/machinebroker/apiutils"
13+
volumebrokerv1alpha1 "github.com/ironcore-dev/ironcore/broker/volumebroker/api/v1alpha1"
14+
irimeta "github.com/ironcore-dev/ironcore/iri/apis/meta/v1alpha1"
15+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
16+
poolletutils "github.com/ironcore-dev/ironcore/poollet/common/utils"
17+
volumepoolletv1alpha1 "github.com/ironcore-dev/ironcore/poollet/volumepoollet/api/v1alpha1"
18+
corev1 "k8s.io/api/core/v1"
19+
"k8s.io/apimachinery/pkg/api/resource"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
)
23+
24+
var _ = Describe("CreateVolumeSnapshot", func() {
25+
ns, srv := SetupTest()
26+
volumeClass := SetupVolumeClass()
27+
28+
It("should create a volume snapshot", func(ctx SpecContext) {
29+
By("creating a volume")
30+
volume := &storagev1alpha1.Volume{
31+
ObjectMeta: metav1.ObjectMeta{
32+
Namespace: ns.Name,
33+
Name: "test-volume",
34+
Labels: map[string]string{
35+
volumebrokerv1alpha1.ManagerLabel: volumebrokerv1alpha1.VolumeBrokerManager,
36+
volumebrokerv1alpha1.CreatedLabel: "true",
37+
},
38+
},
39+
Spec: storagev1alpha1.VolumeSpec{
40+
VolumeClassRef: &corev1.LocalObjectReference{
41+
Name: volumeClass.Name,
42+
},
43+
Resources: corev1alpha1.ResourceList{
44+
corev1alpha1.ResourceStorage: resource.MustParse("1Gi"),
45+
},
46+
},
47+
}
48+
Expect(k8sClient.Create(ctx, volume)).To(Succeed())
49+
By("creating a volume snapshot")
50+
req := &iri.CreateVolumeSnapshotRequest{
51+
VolumeSnapshot: &iri.VolumeSnapshot{
52+
Metadata: &irimeta.ObjectMetadata{
53+
Labels: map[string]string{
54+
volumepoolletv1alpha1.VolumeSnapshotUIDLabel: "foobar",
55+
},
56+
},
57+
Spec: &iri.VolumeSnapshotSpec{
58+
VolumeId: volume.Name,
59+
},
60+
},
61+
}
62+
63+
res, err := srv.CreateVolumeSnapshot(ctx, req)
64+
Expect(err).NotTo(HaveOccurred())
65+
Expect(res).NotTo(BeNil())
66+
Expect(res.VolumeSnapshot).NotTo(BeNil())
67+
68+
By("getting the ironcore volume snapshot")
69+
ironcoreVolumeSnapshot := &storagev1alpha1.VolumeSnapshot{}
70+
ironcoreVolumeSnapshotKey := client.ObjectKey{Namespace: ns.Name, Name: res.VolumeSnapshot.Metadata.Id}
71+
Expect(k8sClient.Get(ctx, ironcoreVolumeSnapshotKey, ironcoreVolumeSnapshot)).To(Succeed())
72+
73+
By("inspecting the ironcore volume snapshot")
74+
Expect(ironcoreVolumeSnapshot.Labels).To(Equal(map[string]string{
75+
poolletutils.DownwardAPILabel(volumepoolletv1alpha1.VolumeSnapshotDownwardAPIPrefix, "root-volume-snapshot-uid"): "foobar",
76+
volumebrokerv1alpha1.CreatedLabel: "true",
77+
volumebrokerv1alpha1.ManagerLabel: volumebrokerv1alpha1.VolumeBrokerManager,
78+
}))
79+
encodedIRIAnnotations, err := apiutils.EncodeAnnotationsAnnotation(nil)
80+
Expect(err).NotTo(HaveOccurred())
81+
encodedIRILabels, err := apiutils.EncodeLabelsAnnotation(map[string]string{
82+
volumepoolletv1alpha1.VolumeSnapshotUIDLabel: "foobar",
83+
})
84+
Expect(err).NotTo(HaveOccurred())
85+
Expect(ironcoreVolumeSnapshot.Annotations).To(Equal(map[string]string{
86+
volumebrokerv1alpha1.AnnotationsAnnotation: encodedIRIAnnotations,
87+
volumebrokerv1alpha1.LabelsAnnotation: encodedIRILabels,
88+
}))
89+
Expect(ironcoreVolumeSnapshot.Spec.VolumeRef.Name).To(Equal(volume.Name))
90+
})
91+
92+
})

0 commit comments

Comments
 (0)