Skip to content

Commit 401cbc4

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

12 files changed

+1436
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-FileCopyrightText: 2023 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+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-FileCopyrightText: 2023 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+
volumebrokerv1alpha1 "github.com/ironcore-dev/ironcore/broker/volumebroker/api/v1alpha1"
13+
"github.com/ironcore-dev/ironcore/broker/volumebroker/apiutils"
14+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
15+
utilsmaps "github.com/ironcore-dev/ironcore/utils/maps"
16+
"google.golang.org/grpc/codes"
17+
"google.golang.org/grpc/status"
18+
corev1 "k8s.io/api/core/v1"
19+
apierrors "k8s.io/apimachinery/pkg/api/errors"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
)
23+
24+
func (s *Server) getIronCoreVolumeSnapshotConfig(ctx context.Context, volumeSnapshot *iri.VolumeSnapshot) (*storagev1alpha1.VolumeSnapshot, error) {
25+
volumeID := volumeSnapshot.Spec.VolumeId
26+
if volumeID == "" {
27+
return nil, status.Errorf(codes.InvalidArgument, "volume ID is required")
28+
}
29+
30+
volume := &storagev1alpha1.Volume{}
31+
if err := s.getManagedAndCreated(ctx, volumeID, volume); err != nil {
32+
if !apierrors.IsNotFound(err) {
33+
return nil, fmt.Errorf("error getting volume %s: %w", volumeID, err)
34+
}
35+
return nil, status.Errorf(codes.NotFound, "volume with ID %s not found", volumeID)
36+
}
37+
38+
ironcoreVolumeSnapshot := &storagev1alpha1.VolumeSnapshot{
39+
ObjectMeta: metav1.ObjectMeta{
40+
Namespace: s.namespace,
41+
Name: s.idGen.Generate(),
42+
Labels: utilsmaps.AppendMap(volumeSnapshot.Metadata.Labels, map[string]string{
43+
volumebrokerv1alpha1.ManagerLabel: volumebrokerv1alpha1.VolumeBrokerManager,
44+
}),
45+
Annotations: volumeSnapshot.Metadata.Annotations,
46+
},
47+
Spec: storagev1alpha1.VolumeSnapshotSpec{
48+
VolumeRef: &corev1.LocalObjectReference{
49+
Name: volume.Name,
50+
},
51+
},
52+
}
53+
54+
if err := apiutils.SetObjectMetadata(ironcoreVolumeSnapshot, volumeSnapshot.Metadata); err != nil {
55+
return nil, err
56+
}
57+
58+
return ironcoreVolumeSnapshot, nil
59+
}
60+
61+
func (s *Server) createIronCoreVolumeSnapshot(ctx context.Context, log logr.Logger, volumeSnapshot *storagev1alpha1.VolumeSnapshot) (retErr error) {
62+
c, cleanup := s.setupCleaner(ctx, log, &retErr)
63+
defer cleanup()
64+
65+
log.V(1).Info("Creating ironcore volume snapshot")
66+
if err := s.client.Create(ctx, volumeSnapshot); err != nil {
67+
return fmt.Errorf("error creating ironcore volume snapshot: %w", err)
68+
}
69+
c.Add(func(ctx context.Context) error {
70+
if err := s.client.Delete(ctx, volumeSnapshot); client.IgnoreNotFound(err) != nil {
71+
return fmt.Errorf("error deleting ironcore volume snapshot: %w", err)
72+
}
73+
return nil
74+
})
75+
76+
log.V(1).Info("Patching ironcore volume snapshot as created")
77+
if err := apiutils.PatchCreated(ctx, s.client, volumeSnapshot); err != nil {
78+
return fmt.Errorf("error patching ironcore volume snapshot as created: %w", err)
79+
}
80+
81+
// Reset cleaner since everything from now on operates on a consistent volume snapshot
82+
c.Reset()
83+
84+
return nil
85+
}
86+
87+
func (s *Server) CreateVolumeSnapshot(ctx context.Context, req *iri.CreateVolumeSnapshotRequest) (res *iri.CreateVolumeSnapshotResponse, retErr error) {
88+
log := s.loggerFrom(ctx)
89+
90+
log.V(1).Info("Getting volume snapshot configuration")
91+
ironcoreVolumeSnapshot, err := s.getIronCoreVolumeSnapshotConfig(ctx, req.VolumeSnapshot)
92+
if err != nil {
93+
return nil, fmt.Errorf("error getting ironcore volume snapshot config: %w", err)
94+
}
95+
96+
if err := s.createIronCoreVolumeSnapshot(ctx, log, ironcoreVolumeSnapshot); err != nil {
97+
return nil, fmt.Errorf("error creating ironcore volume snapshot: %w", err)
98+
}
99+
100+
v, err := s.convertIronCoreVolumeSnapshot(ctx, ironcoreVolumeSnapshot)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
return &iri.CreateVolumeSnapshotResponse{
106+
VolumeSnapshot: v,
107+
}, nil
108+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// SPDX-FileCopyrightText: 2023 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+
"context"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
13+
14+
corev1alpha1 "github.com/ironcore-dev/ironcore/api/core/v1alpha1"
15+
storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
16+
volumebrokerv1alpha1 "github.com/ironcore-dev/ironcore/broker/volumebroker/api/v1alpha1"
17+
irimeta "github.com/ironcore-dev/ironcore/iri/apis/meta/v1alpha1"
18+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
19+
corev1 "k8s.io/api/core/v1"
20+
"k8s.io/apimachinery/pkg/api/resource"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
"sigs.k8s.io/controller-runtime/pkg/client"
23+
)
24+
25+
var _ = Describe("CreateVolumeSnapshot", func() {
26+
ns, srv := SetupTest()
27+
volumeClass := SetupVolumeClass()
28+
29+
var (
30+
volume *storagev1alpha1.Volume
31+
)
32+
33+
BeforeEach(func() {
34+
35+
volume = &storagev1alpha1.Volume{
36+
ObjectMeta: metav1.ObjectMeta{
37+
Namespace: ns.Name,
38+
Name: "test-volume",
39+
Labels: map[string]string{
40+
volumebrokerv1alpha1.ManagerLabel: volumebrokerv1alpha1.VolumeBrokerManager,
41+
volumebrokerv1alpha1.CreatedLabel: "true",
42+
},
43+
},
44+
Spec: storagev1alpha1.VolumeSpec{
45+
VolumeClassRef: &corev1.LocalObjectReference{
46+
Name: volumeClass.Name,
47+
},
48+
Resources: corev1alpha1.ResourceList{
49+
corev1alpha1.ResourceStorage: resource.MustParse("1Gi"),
50+
},
51+
},
52+
Status: storagev1alpha1.VolumeStatus{
53+
State: storagev1alpha1.VolumeStateAvailable,
54+
VolumeID: "test-volume",
55+
},
56+
}
57+
Expect(k8sClient.Create(context.Background(), volume)).To(Succeed())
58+
})
59+
60+
AfterEach(func() {
61+
Expect(k8sClient.Delete(context.Background(), volume)).To(Succeed())
62+
})
63+
64+
It("should create a volume snapshot", func(ctx SpecContext) {
65+
By("creating a volume snapshot")
66+
req := &iri.CreateVolumeSnapshotRequest{
67+
VolumeSnapshot: &iri.VolumeSnapshot{
68+
Metadata: &irimeta.ObjectMetadata{
69+
Labels: map[string]string{
70+
"test-label": "test-value",
71+
},
72+
},
73+
Spec: &iri.VolumeSnapshotSpec{
74+
VolumeId: volume.Status.VolumeID,
75+
},
76+
},
77+
}
78+
79+
res, err := srv.CreateVolumeSnapshot(ctx, req)
80+
Expect(err).NotTo(HaveOccurred())
81+
Expect(res).NotTo(BeNil())
82+
Expect(res.VolumeSnapshot).NotTo(BeNil())
83+
Expect(res.VolumeSnapshot.Spec.VolumeId).To(Equal(volume.Status.VolumeID))
84+
85+
By("getting the ironcore volume snapshot")
86+
var volumeSnapshotList storagev1alpha1.VolumeSnapshotList
87+
Expect(k8sClient.List(ctx, &volumeSnapshotList, client.InNamespace(ns.Name))).To(Succeed())
88+
Expect(volumeSnapshotList.Items).To(HaveLen(1))
89+
90+
volumeSnapshot := volumeSnapshotList.Items[0]
91+
Expect(volumeSnapshot.Spec.VolumeRef.Name).To(Equal(volume.Name))
92+
Expect(volumeSnapshot.Labels).To(HaveKeyWithValue(volumebrokerv1alpha1.ManagerLabel, volumebrokerv1alpha1.VolumeBrokerManager))
93+
Expect(volumeSnapshot.Labels).To(HaveKeyWithValue(volumebrokerv1alpha1.CreatedLabel, "true"))
94+
})
95+
96+
It("should return error if volume ID is empty", func(ctx SpecContext) {
97+
By("creating a volume snapshot with empty volume ID")
98+
req := &iri.CreateVolumeSnapshotRequest{
99+
VolumeSnapshot: &iri.VolumeSnapshot{
100+
Metadata: &irimeta.ObjectMetadata{},
101+
Spec: &iri.VolumeSnapshotSpec{
102+
VolumeId: "",
103+
},
104+
},
105+
}
106+
107+
res, err := srv.CreateVolumeSnapshot(ctx, req)
108+
Expect(err).To(HaveOccurred())
109+
Expect(res).To(BeNil())
110+
Expect(status.Code(err)).To(Equal(codes.InvalidArgument))
111+
})
112+
113+
It("should return error if volume is not found", func(ctx SpecContext) {
114+
By("creating a volume snapshot with non-existent volume ID")
115+
req := &iri.CreateVolumeSnapshotRequest{
116+
VolumeSnapshot: &iri.VolumeSnapshot{
117+
Metadata: &irimeta.ObjectMetadata{},
118+
Spec: &iri.VolumeSnapshotSpec{
119+
VolumeId: "non-existent-volume-id",
120+
},
121+
},
122+
}
123+
124+
res, err := srv.CreateVolumeSnapshot(ctx, req)
125+
Expect(err).To(HaveOccurred())
126+
Expect(res).To(BeNil())
127+
Expect(status.Code(err)).To(Equal(codes.NotFound))
128+
})
129+
130+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-FileCopyrightText: 2023 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+
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
11+
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
13+
apierrors "k8s.io/apimachinery/pkg/api/errors"
14+
)
15+
16+
func (s *Server) DeleteVolumeSnapshot(ctx context.Context, req *iri.DeleteVolumeSnapshotRequest) (*iri.DeleteVolumeSnapshotResponse, error) {
17+
volumeSnapshotID := req.VolumeSnapshotId
18+
log := s.loggerFrom(ctx, "VolumeSnapshotID", volumeSnapshotID)
19+
20+
ironcoreVolumeSnapshot, err := s.getIronCoreVolumeSnapshot(ctx, req.VolumeSnapshotId)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
log.V(1).Info("Deleting volume snapshot")
26+
if err := s.client.Delete(ctx, ironcoreVolumeSnapshot); err != nil {
27+
if !apierrors.IsNotFound(err) {
28+
return nil, fmt.Errorf("error deleting ironcore volume snapshot: %w", err)
29+
}
30+
return nil, status.Errorf(codes.NotFound, "volume snapshot %s not found", volumeSnapshotID)
31+
}
32+
33+
return &iri.DeleteVolumeSnapshotResponse{}, nil
34+
}

0 commit comments

Comments
 (0)