Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api/storage/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ package v1alpha1
import corev1 "k8s.io/api/core/v1"

const (
VolumeVolumePoolRefNameField = "spec.volumePoolRef.name"
VolumeVolumeClassRefNameField = "spec.volumeClassRef.name"
VolumeVolumePoolRefNameField = "spec.volumePoolRef.name"
VolumeVolumeClassRefNameField = "spec.volumeClassRef.name"
VolumeVolumeSnapshotRefNameField = "spec.volumeSnapshotRef.name"

BucketBucketPoolRefNameField = "spec.bucketPoolRef.name"
BucketBucketClassRefNameField = "spec.bucketClassRef.name"
Expand Down
3 changes: 2 additions & 1 deletion broker/volumebroker/server/server_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ func SetupTest() (*corev1.Namespace, *server.Server) {

newSrv, err := server.New(cfg, server.Options{
BrokerDownwardAPILabels: map[string]string{
"root-volume-uid": volumepoolletv1alpha1.VolumeUIDLabel,
"root-volume-uid": volumepoolletv1alpha1.VolumeUIDLabel,
"root-volume-snapshot-uid": volumepoolletv1alpha1.VolumeSnapshotUIDLabel,
},
Namespace: ns.Name,
VolumePoolName: volumePool.Name,
Expand Down
30 changes: 28 additions & 2 deletions broker/volumebroker/server/volume_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
volumepoolletv1alpha1.VolumeDownwardAPIPrefix,
)

var image string
var volumeSnapshotRef *corev1.LocalObjectReference

image = volume.Spec.Image // TODO: Remove this once volume.Spec.Image is deprecated

if dataSource := volume.Spec.VolumeDataSource; dataSource != nil {
switch {
case dataSource.SnapshotDataSource != nil:
volumeSnapshotRef = &corev1.LocalObjectReference{Name: dataSource.SnapshotDataSource.SnapshotId}
image = "" // TODO: Remove this once volume.Spec.Image is deprecated
case dataSource.ImageDataSource != nil:
image = dataSource.ImageDataSource.Image
}
}

ironcoreVolume := &storagev1alpha1.Volume{
ObjectMeta: metav1.ObjectMeta{
Namespace: s.namespace,
Expand All @@ -81,9 +96,13 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
Resources: corev1alpha1.ResourceList{
corev1alpha1.ResourceStorage: *resource.NewQuantity(volume.Spec.Resources.StorageBytes, resource.DecimalSI),
},
Image: volume.Spec.Image,
ImagePullSecretRef: nil, // TODO: Fill if necessary
Image: image, // TODO: Remove this once volume.Spec.Image is deprecated
ImagePullSecretRef: nil, // TODO: Fill if necessary
Encryption: encryption,
VolumeDataSource: storagev1alpha1.VolumeDataSource{
VolumeSnapshotRef: volumeSnapshotRef,
OSImage: getOSImageIfPresent(image),
},
},
}
if err := apiutils.SetObjectMetadata(ironcoreVolume, volume.Metadata); err != nil {
Expand All @@ -96,6 +115,13 @@ func (s *Server) getIronCoreVolumeConfig(_ context.Context, volume *iri.Volume)
}, nil
}

func getOSImageIfPresent(image string) *string {
if image == "" {
return nil
}
return &image
}

func (s *Server) createIronCoreVolume(ctx context.Context, log logr.Logger, volume *AggregateIronCoreVolume) (retErr error) {
c, cleanup := s.setupCleaner(ctx, log, &retErr)
defer cleanup()
Expand Down
56 changes: 55 additions & 1 deletion broker/volumebroker/server/volume_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -67,4 +68,57 @@ var _ = Describe("CreateVolume", func() {
Expect(ironcoreVolume.Spec.VolumeClassRef.Name).To(Equal(volumeClass.Name))
Expect(ironcoreVolume.Spec.Resources).To(HaveLen(1))
})

It("should correctly create a volume from snapshot", func(ctx SpecContext) {
By("creating a volume snapshot")
volumeSnapshot := &storagev1alpha1.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Name: "test-snapshot",
},
Spec: storagev1alpha1.VolumeSnapshotSpec{
VolumeRef: &corev1.LocalObjectReference{Name: "source-volume"},
},
Status: storagev1alpha1.VolumeSnapshotStatus{
State: storagev1alpha1.VolumeSnapshotStateReady,
SnapshotID: "test-snapshot-id",
},
}
Expect(k8sClient.Create(ctx, volumeSnapshot)).To(Succeed())

By("creating a volume with snapshot data source")
res, err := srv.CreateVolume(ctx, &iri.CreateVolumeRequest{
Volume: &iri.Volume{
Metadata: &irimeta.ObjectMetadata{
Labels: map[string]string{
volumepoolletv1alpha1.VolumeUIDLabel: "foobar",
},
},
Spec: &iri.VolumeSpec{
Class: volumeClass.Name,
Resources: &iri.VolumeResources{
StorageBytes: 100,
},
VolumeDataSource: &iri.VolumeDataSource{
SnapshotDataSource: &iri.SnapshotDataSource{
SnapshotId: "test-snapshot",
},
},
},
},
})

Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil())

By("getting the ironcore volume")
ironcoreVolume := &storagev1alpha1.Volume{}
ironcoreVolumeKey := client.ObjectKey{Namespace: ns.Name, Name: res.Volume.Metadata.Id}
Expect(k8sClient.Get(ctx, ironcoreVolumeKey, ironcoreVolume)).To(Succeed())

By("verifying the volume has the correct snapshot reference")
Expect(ironcoreVolume.Spec.VolumeDataSource.VolumeSnapshotRef).NotTo(BeNil())
Expect(ironcoreVolume.Spec.VolumeDataSource.VolumeSnapshotRef.Name).To(Equal("test-snapshot"))
})

})
58 changes: 58 additions & 0 deletions broker/volumebroker/server/volumesnapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package server

import (
"fmt"

storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
"github.com/ironcore-dev/ironcore/broker/volumebroker/apiutils"
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
)

func (s *Server) convertIronCoreVolumeSnapshot(ironcoreVolumeSnapshot *storagev1alpha1.VolumeSnapshot) (*iri.VolumeSnapshot, error) {
metadata, err := apiutils.GetObjectMetadata(ironcoreVolumeSnapshot)
if err != nil {
return nil, fmt.Errorf("error getting object metadata: %w", err)
}

var volumeID string
if ironcoreVolumeSnapshot.Spec.VolumeRef != nil {
volumeID = ironcoreVolumeSnapshot.Spec.VolumeRef.Name
}

state, err := s.convertIronCoreVolumeSnapshotState(ironcoreVolumeSnapshot.Status.State)
if err != nil {
return nil, fmt.Errorf("error converting volume snapshot state: %w", err)
}

iriVolumeSnapshot := &iri.VolumeSnapshot{
Metadata: metadata,
Spec: &iri.VolumeSnapshotSpec{
VolumeId: volumeID,
},
Status: &iri.VolumeSnapshotStatus{
State: state,
},
}

if ironcoreVolumeSnapshot.Status.Size != nil {
iriVolumeSnapshot.Status.Size = ironcoreVolumeSnapshot.Status.Size.Value()
}

return iriVolumeSnapshot, nil
}

var ironcoreVolumeSnapshotStateToIRIState = map[storagev1alpha1.VolumeSnapshotState]iri.VolumeSnapshotState{
storagev1alpha1.VolumeSnapshotStatePending: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_PENDING,
storagev1alpha1.VolumeSnapshotStateReady: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_READY,
storagev1alpha1.VolumeSnapshotStateFailed: iri.VolumeSnapshotState_VOLUME_SNAPSHOT_FAILED,
}

func (s *Server) convertIronCoreVolumeSnapshotState(state storagev1alpha1.VolumeSnapshotState) (iri.VolumeSnapshotState, error) {
if state, ok := ironcoreVolumeSnapshotStateToIRIState[state]; ok {
return state, nil
}
return 0, fmt.Errorf("unknown ironcore volume snapshot state %q", state)
}
116 changes: 116 additions & 0 deletions broker/volumebroker/server/volumesnapshot_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package server

import (
"context"
"fmt"

"github.com/go-logr/logr"
storagev1alpha1 "github.com/ironcore-dev/ironcore/api/storage/v1alpha1"
brokerutils "github.com/ironcore-dev/ironcore/broker/common/utils"
volumebrokerv1alpha1 "github.com/ironcore-dev/ironcore/broker/volumebroker/api/v1alpha1"
"github.com/ironcore-dev/ironcore/broker/volumebroker/apiutils"
iri "github.com/ironcore-dev/ironcore/iri/apis/volume/v1alpha1"
volumepoolletv1alpha1 "github.com/ironcore-dev/ironcore/poollet/volumepoollet/api/v1alpha1"
utilsmaps "github.com/ironcore-dev/ironcore/utils/maps"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func (s *Server) getIronCoreVolumeSnapshotConfig(ctx context.Context, volumeSnapshot *iri.VolumeSnapshot) (*storagev1alpha1.VolumeSnapshot, error) {
volumeID := volumeSnapshot.Spec.VolumeId
if volumeID == "" {
return nil, status.Errorf(codes.InvalidArgument, "volume ID is required")
}

volume := &storagev1alpha1.Volume{}
if err := s.getManagedAndCreated(ctx, volumeID, volume); err != nil {
if apierrors.IsNotFound(err) {
return nil, status.Errorf(codes.NotFound, "volume with ID %s not found", volumeID)
}
return nil, fmt.Errorf("error getting volume %s: %w", volumeID, err)
}

labels := brokerutils.PrepareDownwardAPILabels(
volumeSnapshot.Metadata.Labels,
s.brokerDownwardAPILabels,
volumepoolletv1alpha1.VolumeSnapshotDownwardAPIPrefix,
)

ironcoreVolumeSnapshot := &storagev1alpha1.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Namespace: s.namespace,
Name: s.idGen.Generate(),
Labels: utilsmaps.AppendMap(labels, map[string]string{
volumebrokerv1alpha1.ManagerLabel: volumebrokerv1alpha1.VolumeBrokerManager,
}),
Annotations: volumeSnapshot.Metadata.Annotations,
},
Spec: storagev1alpha1.VolumeSnapshotSpec{
VolumeRef: &corev1.LocalObjectReference{
Name: volume.Name,
},
},
}

if err := apiutils.SetObjectMetadata(ironcoreVolumeSnapshot, volumeSnapshot.Metadata); err != nil {
return nil, err
}

return ironcoreVolumeSnapshot, nil
}

func (s *Server) createIronCoreVolumeSnapshot(ctx context.Context, log logr.Logger, volumeSnapshot *storagev1alpha1.VolumeSnapshot) (retErr error) {
c, cleanup := s.setupCleaner(ctx, log, &retErr)
defer cleanup()

log.V(1).Info("Creating ironcore volume snapshot")
if err := s.client.Create(ctx, volumeSnapshot); err != nil {
return fmt.Errorf("error creating ironcore volume snapshot: %w", err)
}
c.Add(func(ctx context.Context) error {
if err := s.client.Delete(ctx, volumeSnapshot); client.IgnoreNotFound(err) != nil {
return fmt.Errorf("error deleting ironcore volume snapshot: %w", err)
}
return nil
})

log.V(1).Info("Patching ironcore volume snapshot as created")
if err := apiutils.PatchCreated(ctx, s.client, volumeSnapshot); err != nil {
return fmt.Errorf("error patching ironcore volume snapshot as created: %w", err)
}

// Reset cleaner since everything from now on operates on a consistent volume snapshot
c.Reset()

return nil
}

func (s *Server) CreateVolumeSnapshot(ctx context.Context, req *iri.CreateVolumeSnapshotRequest) (res *iri.CreateVolumeSnapshotResponse, retErr error) {
log := s.loggerFrom(ctx)

log.V(1).Info("Getting volume snapshot configuration")
ironcoreVolumeSnapshot, err := s.getIronCoreVolumeSnapshotConfig(ctx, req.VolumeSnapshot)
if err != nil {
return nil, fmt.Errorf("error getting ironcore volume snapshot config: %w", err)
}

if err := s.createIronCoreVolumeSnapshot(ctx, log, ironcoreVolumeSnapshot); err != nil {
return nil, fmt.Errorf("error creating ironcore volume snapshot: %w", err)
}

v, err := s.convertIronCoreVolumeSnapshot(ironcoreVolumeSnapshot)
if err != nil {
return nil, err
}

return &iri.CreateVolumeSnapshotResponse{
VolumeSnapshot: v,
}, nil
}
Loading
Loading