diff --git a/go.mod b/go.mod index 94b3586..7e7b345 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/BurntSushi/toml v0.3.1 - github.com/civo/civogo v0.3.92 + github.com/civo/civogo v0.3.93 github.com/container-storage-interface/spec v1.6.0 github.com/golang/protobuf v1.5.4 github.com/joho/godotenv v1.4.0 @@ -15,6 +15,7 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/sys v0.18.0 google.golang.org/grpc v1.56.3 + google.golang.org/protobuf v1.33.0 k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 k8s.io/client-go v0.29.0 @@ -57,7 +58,6 @@ require ( golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 4929a94..6aa5c53 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,8 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/civo/civogo v0.3.67 h1:R6MepF20Od7KQdcEKpr3GD8zKy1Jly0SuBDubZkEU2s= -github.com/civo/civogo v0.3.67/go.mod h1:S/iYmGvQOraxdRtcXeq/2mVX01/ia2qfpQUp2SsTLKA= -github.com/civo/civogo v0.3.92 h1:fiA9nsyvEU9vVwA8ssIDSGt6gsWs6meqgaKrimAEaI0= -github.com/civo/civogo v0.3.92/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM= +github.com/civo/civogo v0.3.93 h1:wxzMamDKYu2lszObvx92tTFDpi0sCJbDO+CL3cR/P28= +github.com/civo/civogo v0.3.93/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= diff --git a/pkg/driver/controller_server.go b/pkg/driver/controller_server.go index e57381f..37f2948 100644 --- a/pkg/driver/controller_server.go +++ b/pkg/driver/controller_server.go @@ -3,6 +3,7 @@ package driver import ( "context" "fmt" + "sort" "strings" "time" @@ -11,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" ) // BytesInGigabyte describes how many bytes are in a gigabyte @@ -95,21 +97,20 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) } } - // TODO: Uncomment after client implementation is complete. - // snapshotID := "" - // if volSource := req.GetVolumeContentSource(); volSource != nil { - // if _, ok := volSource.GetType().(*csi.VolumeContentSource_Snapshot); !ok { - // return nil, status.Error(codes.InvalidArgument, "Unsupported volumeContentSource type") - // } - // snapshot := volSource.GetSnapshot() - // if snapshot == nil { - // return nil, status.Error(codes.InvalidArgument, "Volume content source type is set to Snapshot, but the Snapshot is not provided") - // } - // snapshotID = snapshot.GetSnapshotId() - // if snapshotID == "" { - // return nil, status.Error(codes.InvalidArgument, "Volume content source type is set to Snapshot, but the SnapshotID is not provided") - // } - // } + snapshotID := "" + if volSource := req.GetVolumeContentSource(); volSource != nil { + if _, ok := volSource.GetType().(*csi.VolumeContentSource_Snapshot); !ok { + return nil, status.Error(codes.InvalidArgument, "Unsupported volumeContentSource type") + } + snapshot := volSource.GetSnapshot() + if snapshot == nil { + return nil, status.Error(codes.InvalidArgument, "Volume content source type is set to Snapshot, but the Snapshot is not provided") + } + snapshotID = snapshot.GetSnapshotId() + if snapshotID == "" { + return nil, status.Error(codes.InvalidArgument, "Volume content source type is set to Snapshot, but the SnapshotID is not provided") + } + } log.Debug().Msg("Volume doesn't currently exist, will need creating") @@ -136,7 +137,7 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) Namespace: d.Namespace, ClusterID: d.ClusterID, SizeGigabytes: int(desiredSize), - // SnapshotID: snapshotID, // TODO: Uncomment after client implementation is complete. + SnapshotID: snapshotID, } log.Debug().Msg("Creating volume in Civo API") result, err := d.CivoClient.NewVolume(v) @@ -585,8 +586,8 @@ func (d *Driver) ControllerGetCapabilities(context.Context, *csi.ControllerGetCa csi.ControllerServiceCapability_RPC_LIST_VOLUMES, csi.ControllerServiceCapability_RPC_GET_CAPACITY, csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, - // csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, TODO: Uncomment after client implementation is complete. - // csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, TODO: Uncomment after client implementation is complete. + csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, + csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, } var csc []*csi.ControllerServiceCapability @@ -612,109 +613,118 @@ func (d *Driver) ControllerGetCapabilities(context.Context, *csi.ControllerGetCa // CreateSnapshot is part of implementing Snapshot & Restore functionality, but we don't support that func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { - return nil, status.Error(codes.Unimplemented, "") - // TODO: Uncomment after client implementation is complete. - // snapshotName := req.GetName() - // sourceVolID := req.GetSourceVolumeId() - // - // log.Info(). - // Str("snapshot_name", snapshotName). - // Str("source_volume_id", sourceVolID). - // Msg("Request: CreateSnapshot") - // - // if len(snapshotName) == 0 { - // return nil, status.Error(codes.InvalidArgument, "Snapshot name is required") - // } - // if len(sourceVolID) == 0 { - // return nil, status.Error(codes.InvalidArgument, "SourceVolumeId is required") - // } - // - // log.Debug(). - // Str("source_volume_id", sourceVolID). - // Msg("Finding current snapshot in Civo API") - // - // snapshots, err := d.CivoClient.ListVolumeSnapshotsByVolumeID(sourceVolID) - // if err != nil { - // log.Error(). - // Str("source_volume_id", sourceVolID). - // Err(err). - // Msg("Unable to list snapshot in Civo API") - // return nil, status.Errorf(codes.Internal, "failed to list snapshots by %q: %s", sourceVolID, err) - // } - // - // // Check for an existing snapshot with the specified name. - // for _, snapshot := range snapshots { - // if snapshot.Name != snapshotName { - // continue - // } - // if snapshot.VolumeID == sourceVolID { - // return &csi.CreateSnapshotResponse{ - // Snapshot: &csi.Snapshot{ - // SnapshotId: snapshot.SnapshotID, - // SourceVolumeId: snapshot.VolumeID, - // CreationTime: snapshot.CreationTime, - // SizeBytes: snapshot.RestoreSize, - // ReadyToUse: true, - // }, - // }, nil - // } - // log.Error(). - // Str("snapshot_name", snapshotName). - // Str("requested_source_volume_id", sourceVolID). - // Str("actual_source_volume_id", snapshot.VolumeID). - // Err(err). - // Msg("Snapshot with the same name but with different SourceVolumeId already exist") - // return nil, status.Errorf(codes.AlreadyExists, "snapshot with the same name %q but with different SourceVolumeId already exist", snapshotName) - // } - // - // log.Debug(). - // Str("snapshot_name", snapshotName). - // Str("source_volume_id", sourceVolID). - // Msg("Create volume snapshot in Civo API") - // - // result, err := d.CivoClient.CreateVolumeSnapshot(sourceVolID, &civogo.VolumeSnapshotConfig{ - // Name: snapshotName, - // }) - // if err != nil { - // if strings.Contains(err.Error(), "DatabaseVolumeSnapshotLimitExceededError") { - // log.Error().Err(err).Msg("Requested volume snapshot would exceed volume quota available") - // return nil, status.Errorf(codes.ResourceExhausted, "failed to create volume snapshot due to over quota: %s", err) - // } - // log.Error().Err(err).Msg("Unable to create snapshot in Civo API") - // return nil, status.Errorf(codes.Internal, "failed to create volume snapshot: %s", err) - // } - // - // log.Info(). - // Str("snapshot_id", result.SnapshotID). - // Msg("Snapshot created in Civo API") - // - // // NOTE: Add waitFor logic if creation takes long time. - // snapshot, err := d.CivoClient.GetVolumeSnapshot(result.SnapshotID) - // if err != nil { - // log.Error(). - // Str("snapshot_id", result.SnapshotID). - // Err(err). - // Msg("Unsable to get snapshot updates from Civo API") - // return nil, status.Errorf(codes.Internal, "failed to get snapshot by %q: %s", result.SnapshotID, err) - // } - // return &csi.CreateSnapshotResponse{ - // Snapshot: &csi.Snapshot{ - // SnapshotId: snapshot.SnapshotID, - // SourceVolumeId: snapshot.VolumeID, - // CreationTime: snapshot.CreationTime, - // SizeBytes: snapshot.RestoreSize, - // ReadyToUse: true, - // }, - // }, nil + snapshotName := req.GetName() + sourceVolID := req.GetSourceVolumeId() + + log.Info(). + Str("snapshot_name", snapshotName). + Str("source_volume_id", sourceVolID). + Msg("Request: CreateSnapshot") + + if len(snapshotName) == 0 { + return nil, status.Error(codes.InvalidArgument, "Snapshot name is required") + } + if len(sourceVolID) == 0 { + return nil, status.Error(codes.InvalidArgument, "SourceVolumeId is required") + } + + log.Debug(). + Str("source_volume_id", sourceVolID). + Msg("Finding current snapshot in Civo API") + + snapshots, err := d.CivoClient.ListVolumeSnapshots() + if err != nil { + log.Error(). + Str("source_volume_id", sourceVolID). + Err(err). + Msg("Unable to list snapshot in Civo API") + return nil, status.Errorf(codes.Internal, "failed to list snapshots by %q: %s", sourceVolID, err) + } + + // Check for an existing snapshot with the specified name. + for _, snapshot := range snapshots { + if snapshot.Name != snapshotName { + continue + } + if snapshot.VolumeID == sourceVolID { + snap, err := ToCSISnapshot(&snapshot) + if err != nil{ + log.Error(). + Str("snapshot_name", snapshotName). + Str("source_volume_id", sourceVolID). + Err(err). + Msg("filed to convert civo snapshot to csi snapshot") + return nil, status.Errorf(codes.Internal, "filed to convert civo snapshot %s to csi snapshot: %v", snapshot.SnapshotID, err) + } + return &csi.CreateSnapshotResponse{ + Snapshot: snap, + }, nil + } + log.Error(). + Str("snapshot_name", snapshotName). + Str("requested_source_volume_id", sourceVolID). + Str("actual_source_volume_id", snapshot.VolumeID). + Msg("Snapshot with the same name but with different SourceVolumeId already exist") + return nil, status.Errorf(codes.AlreadyExists, "snapshot with the same name %q but with different SourceVolumeId already exist", snapshotName) + } + + log.Debug(). + Str("snapshot_name", snapshotName). + Str("source_volume_id", sourceVolID). + Msg("Create volume snapshot in Civo API") + + result, err := d.CivoClient.CreateVolumeSnapshot(sourceVolID, &civogo.VolumeSnapshotConfig{ + Name: snapshotName, + }) + if err != nil { + if strings.Contains(err.Error(), "DatabaseVolumeSnapshotLimitExceededError") { + log.Error().Err(err).Msg("Requested volume snapshot would exceed volume quota available") + return nil, status.Errorf(codes.ResourceExhausted, "failed to create volume snapshot due to over quota: %s", err) + } + log.Error().Err(err).Msg("Unable to create snapshot in Civo API") + return nil, status.Errorf(codes.Internal, "failed to create volume snapshot: %s", err) + } + + log.Info(). + Str("snapshot_id", result.SnapshotID). + Msg("Snapshot created in Civo API") + + // NOTE: Add waitFor logic if creation takes long time. + time.Sleep(5 * time.Second) + snapshot, err := d.CivoClient.GetVolumeSnapshot(result.SnapshotID) + if err != nil { + log.Error(). + Str("snapshot_id", result.SnapshotID). + Err(err). + Msg("Unsable to get snapshot updates from Civo API") + return nil, status.Errorf(codes.Internal, "failed to get snapshot by %q: %s", result.SnapshotID, err) + } + creationTime, err := ParseTimeToProtoTimestamp(snapshot.CreationTime) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to parse creation time: %v", err)) + } + + isReady := IsSnapshotReady(snapshot.State) + + return &csi.CreateSnapshotResponse{ + Snapshot: &csi.Snapshot{ + SnapshotId: snapshot.SnapshotID, + SourceVolumeId: snapshot.VolumeID, + CreationTime: creationTime, + SizeBytes: int64(snapshot.RestoreSize), + ReadyToUse: isReady, + }, + }, nil } // DeleteSnapshot is part of implementing Snapshot & Restore functionality, and it will be supported in the future. func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { + snapshotID := req.GetSnapshotId() + log.Info(). Str("snapshot_id", req.GetSnapshotId()). Msg("Request: DeleteSnapshot") - snapshotID := req.GetSnapshotId() if snapshotID == "" { return nil, status.Error(codes.InvalidArgument, "must provide SnapshotId to DeleteSnapshot") } @@ -723,21 +733,19 @@ func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequ Str("snapshot_id", snapshotID). Msg("Deleting snapshot in Civo API") - // TODO: Uncomment after client implementation is complete. - // _, err := d.CivoClient.DeleteVolumeSnapshot(snapshotID) - // if err != nil { - // if strings.Contains(err.Error(), "DatabaseVolumeSnapshotNotFoundError") { - // log.Info(). - // Str("volume_id", snapshotID). - // Msg("Snapshot already deleted from Civo API") - // return &csi.DeleteSnapshotResponse{}, nil - // } else if strings.Contains(err.Error(), "DatabaseSnapshotCannotDeleteInUseError") { - // return nil, status.Errorf(codes.FailedPrecondition, "failed to delete snapshot %q, it is currently in use, err: %s", snapshotID, err) - // } - // return nil, status.Errorf(codes.Internal, "failed to delete snapshot %q, err: %s", snapshotID, err) - // } - // return &csi.DeleteSnapshotResponse{}, nil - return nil, status.Error(codes.Unimplemented, "") + _, err := d.CivoClient.DeleteVolumeSnapshot(snapshotID) + if err != nil { + if strings.Contains(err.Error(), "DatabaseVolumeSnapshotNotFoundError") { + log.Info(). + Str("volume_id", snapshotID). + Msg("Snapshot already deleted from Civo API") + return &csi.DeleteSnapshotResponse{}, nil + } else if strings.Contains(err.Error(), "DatabaseSnapshotCannotDeleteInUseError") { + return nil, status.Errorf(codes.FailedPrecondition, "failed to delete snapshot %q, it is currently in use, err: %s", snapshotID, err) + } + return nil, status.Errorf(codes.Internal, "failed to delete snapshot %q, err: %s", snapshotID, err) + } + return &csi.DeleteSnapshotResponse{}, nil } // ListSnapshots retrieves a list of existing snapshots as part of the Snapshot & Restore functionality. @@ -753,91 +761,155 @@ func (d *Driver) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsReques return nil, status.Error(codes.Aborted, "starting-token not supported") } - // case 1: SnapshotId is not empty, return snapshots that match the snapshot id + if len(snapshotID) != 0 && len(sourceVolumeID) != 0 { + log.Debug(). + Str("snapshot_id", snapshotID). + Str("source_volume_id", sourceVolumeID). + Msg("Fetching snapshot") + + snapshot, err := d.CivoClient.GetVolumeSnapshotByVolumeID(sourceVolumeID, snapshotID) + if err != nil { + if strings.Contains(err.Error(), "DatabaseSnapshotNotFoundError") || strings.Contains(err.Error(), "ZeroMatchesError"){ + log.Info(). + Str("snapshot_id", snapshotID). + Str("source_volume_id", sourceVolumeID). + Msg("ListSnapshots: no snapshot found, returning with success") + return &csi.ListSnapshotsResponse{}, nil + } + log.Error(). + Err(err). + Str("snapshot_id", snapshotID). + Str("source_volume_id", sourceVolumeID). + Msg("Failed to list snapshot from Civo API") + return nil, status.Errorf(codes.Internal, "failed to list snapshot %q: %v", snapshotID, err) + } + entry, err := ConvertSnapshot(snapshot) + if err != nil { + log.Error(). + Err(err). + Str("SnapshotID", snapshot.SnapshotID). + Str("VolumeID", snapshot.VolumeID). + Msg("Failed to convert civo snapshot to CSI snapshot") + return nil, status.Errorf(codes.Internal, "failed to convert civo snapshot to CSI snapshot %s: %v", snapshot.SnapshotID, err) + } + + return &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + entry, + }, + }, nil + } + if len(snapshotID) != 0 { log.Debug(). Str("snapshot_id", snapshotID). Msg("Fetching snapshot") - // Retrieve a specific snapshot by ID - // Todo: GetSnapshot to be implemented in civogo - // Todo: Un-comment post client implementation - // snapshot, err := d.CivoClient.GetSnapshot(snapshotID) - // if err != nil { - // // Todo: DatabaseSnapshotNotFoundError & DiskSnapshotNotFoundError are placeholders, it's still not clear what error will be returned by API (awaiting implementation - WIP) - // if strings.Contains(err.Error(), "DatabaseSnapshotNotFoundError") || - // strings.Contains(err.Error(), "DiskSnapshotNotFoundError") { - // log.Info(). - // Str("snapshot_id", snapshotID). - // Msg("ListSnapshots: no snapshot found, returning with success") - // return &csi.ListSnapshotsResponse{}, nil - // } - // log.Error(). - // Err(err). - // Str("snapshot_id", snapshotID). - // Msg("Failed to list snapshot from Civo API") - // return nil, status.Errorf(codes.Internal, "failed to list snapshot %q: %v", snapshotID, err) - // } - // return &csi.ListSnapshotsResponse{ - // Entries: []*csi.ListSnapshotsResponse_Entry{convertSnapshot(snapshot)}, - // }, nil - } - - // case 2: Retrieve snapshots by source volume ID + snapshot, err := d.CivoClient.GetVolumeSnapshot(snapshotID) + if err != nil { + if strings.Contains(err.Error(), "DatabaseSnapshotNotFoundError") || strings.Contains(err.Error(), "ZeroMatchesError"){ + log.Info(). + Str("snapshot_id", snapshotID). + Msg("ListSnapshots: no snapshot found, returning with success") + return &csi.ListSnapshotsResponse{}, nil + } + log.Error(). + Err(err). + Str("snapshot_id", snapshotID). + Msg("Failed to list snapshot from Civo API") + return nil, status.Errorf(codes.Internal, "failed to list snapshot %q: %v", snapshotID, err) + } + + entry, err := ConvertSnapshot(snapshot) + if err != nil { + log.Error(). + Err(err). + Str("SnapshotID", snapshot.SnapshotID). + Str("VolumeID", snapshot.VolumeID). + Msg("Failed to convert civo snapshot to CSI snapshot") + return nil, status.Errorf(codes.Internal, "failed to convert civo snapshot to CSI snapshot %s: %v", snapshot.SnapshotID, err) + } + return &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + entry, + }, + }, nil + } + if len(sourceVolumeID) != 0 { log.Debug(). Str("operation", "list_snapshots"). Str("source_volume_id", sourceVolumeID). Msg("Fetching volume snapshots") - // snapshots, err := d.CivoClient.ListSnapshots() // Todo: ListSnapshots to be implemented in civogo - // if err != nil { - // log.Error(). - // Err(err). - // Str("source_volume_id", sourceVolumeID). - // Msg("Failed to list snapshots for volume") - // return nil, status.Errorf(codes.Internal, "failed to list snapshots for volume %q: %v", sourceVolumeID, err) - // } - // - // entries := []*csi.ListSnapshotsResponse_Entry{} - // for _, snapshot := range snapshots { - // if snapshot.VolID == sourceVolumeID { - // entries = append(entries, convertSnapshot(snapshot)) - // } - // } - // - // return &csi.ListSnapshotsResponse{ - // Entries: entries, - // }, nil + snapshots, err := d.CivoClient.ListVolumeSnapshots() + if err != nil { + log.Error(). + Err(err). + Str("source_volume_id", sourceVolumeID). + Msg("Failed to list snapshots for volume") + return nil, status.Errorf(codes.Internal, "failed to list snapshots for volume %q: %v", sourceVolumeID, err) + } + + entries := []*csi.ListSnapshotsResponse_Entry{} + for _, snapshot := range snapshots { + if snapshot.VolumeID == sourceVolumeID { + entry, err := ConvertSnapshot(&snapshot) + if err != nil { + log.Error(). + Err(err). + Str("SnapshotID", snapshot.SnapshotID). + Str("VolumeID", snapshot.VolumeID). + Msg("Failed to convert civo snapshot to CSI snapshot") + return nil, status.Errorf(codes.Internal, "failed to convert civo snapshot to CSI snapshot %s: %v", snapshot.SnapshotID, err) + } + entries = append(entries, entry) + } + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].GetSnapshot().GetSnapshotId() < entries[j].GetSnapshot().GetSnapshotId() + }) + + return &csi.ListSnapshotsResponse{ + Entries: entries, + }, nil } log.Debug().Msg("Fetching all snapshots") - // case 3: Retrieve all snapshots if no filters are provided - // Todo: un-comment post client(civogo) implementation - // snapshots, err := d.CivoClient.ListSnapshots() // Todo: ListSnapshots to be implemented in civogo - // if err != nil { - // log.Error().Err(err).Msg("Failed to list snapshots from Civo API") - // return nil, status.Errorf(codes.Internal, "failed to list snapshots from Civo API: %v", err) - // } - // - // sort.Slice(snapshots, func(i, j int) bool { - // return snapshots[i].Id < snapshots[j].Id - // }) - // - // entries := []*csi.ListSnapshotsResponse_Entry{} - // for _, snap := range snapshots { - // entries = append(entries, convertSnapshot(snap)) - // } - // - // log.Info(). - // Int("total_snapshots", len(entries)). - // Msg("Snapshots listed successfully") - // - // return &csi.ListSnapshotsResponse{ - // Entries: entries, - // }, nil - return nil, status.Error(codes.Unimplemented, "") + snapshots, err := d.CivoClient.ListVolumeSnapshots() + if err != nil { + log.Error().Err(err).Msg("Failed to list snapshots from Civo API") + return nil, status.Errorf(codes.Internal, "failed to list snapshots from Civo API: %v", err) + } + + sort.Slice(snapshots, func(i, j int) bool { + return snapshots[i].SnapshotID < snapshots[j].SnapshotID + }) + + entries := []*csi.ListSnapshotsResponse_Entry{} + + for _, snap := range snapshots { + entry, err := ConvertSnapshot(&snap) + if err != nil { + log.Error(). + Err(err). + Str("SnapshotID", snap.SnapshotID). + Str("VolumeID", snap.VolumeID). + Msg("Failed to convert civo snapshot to CSI snapshot") + return nil, status.Errorf(codes.Internal, "failed to convert civo snapshot to CSI snapshot %s: %v", snap.SnapshotID, err) + } + entries = append(entries, entry) + + } + + log.Info(). + Int("total_snapshots", len(entries)). + Msg("Snapshots listed successfully") + + return &csi.ListSnapshotsResponse{ + Entries: entries, + }, nil } func getVolSizeInBytes(capRange *csi.CapacityRange) (int64, error) { @@ -854,17 +926,58 @@ func getVolSizeInBytes(capRange *csi.CapacityRange) (int64, error) { return bytes, nil } -// Todo: Un-comment post client implementation is complete -// Todo: Snapshot to be defined in civogo -// convertSnapshot function converts a civogo.Snapshot object(API response) into a CSI ListSnapshotsResponse_Entry -// func convertSnapshot(snap *civogo.Snapshot) *csi.ListSnapshotsResponse_Entry { -// return &csi.ListSnapshotsResponse_Entry{ -// Snapshot: &csi.Snapshot{ -// SnapshotId: snap.Id, -// SourceVolumeId: snap.VolID, -// CreationTime: snap.CreationTime, -// SizeBytes: snap.SizeBytes, -// ReadyToUse: snap.ReadyToUse, -// }, -// } -// } +// ConvertSnapshot function converts a civogo.Snapshot object(API response) into a CSI ListSnapshotsResponse_Entry +func ConvertSnapshot(in *civogo.VolumeSnapshot) (*csi.ListSnapshotsResponse_Entry, error) { + snap, err := ToCSISnapshot(in) + if err != nil{ + return nil, fmt.Errorf("failed to convert civo snapshot %s to csi snapshot: %v", in.SnapshotID, err) + } + + return &csi.ListSnapshotsResponse_Entry{ + Snapshot: snap, + }, nil +} + +// ParseTimeToProtoTimestamp parses a time string in RFC3339 format to *timestamppb.Timestamp. +func ParseTimeToProtoTimestamp(timeStr string) (*timestamppb.Timestamp, error) { + if timeStr == ""{ + return nil, nil + } + t, err := time.Parse(time.RFC3339, timeStr) + // Only return an error if timeStr is not empty + if err != nil && timeStr != "" { + return nil, err + } + return timestamppb.New(t), nil +} + +// IsSnapshotReady determines if a snapshot is ready for use +func IsSnapshotReady(state string) bool { + // Define the states that indicate the snapshot is ready + readyStates := map[string]bool{ + "Ready": true, + "Available": true, // Add other states if applicable + } + // Check if the state exists in the map, return false if not found + _, exists := readyStates[state] + return exists +} + + +func ToCSISnapshot(snap *civogo.VolumeSnapshot)(*csi.Snapshot, error){ + creationTime, err := ParseTimeToProtoTimestamp(snap.CreationTime) + if err != nil { + return nil, fmt.Errorf("failed to parse creation time for snapshot %s: %w", snap.SnapshotID, err) + } + + // Explicitly define which state indicates the snapshot is ready for use + isReady := IsSnapshotReady(snap.State) + + return &csi.Snapshot{ + SnapshotId: snap.SnapshotID, + SourceVolumeId: snap.VolumeID, + CreationTime: creationTime, + SizeBytes: int64(snap.RestoreSize), + ReadyToUse: isReady, + }, nil +} \ No newline at end of file diff --git a/pkg/driver/controller_server_test.go b/pkg/driver/controller_server_test.go index 5f8a9b2..f8c191a 100644 --- a/pkg/driver/controller_server_test.go +++ b/pkg/driver/controller_server_test.go @@ -8,6 +8,8 @@ import ( "github.com/civo/civogo" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func TestCreateVolume(t *testing.T) { @@ -290,3 +292,257 @@ func TestGetCapacity(t *testing.T) { }) } + +func TestCreateSnapshot(t *testing.T){ + tests := []struct { + name string + req *csi.CreateSnapshotRequest + sourceVolume *civogo.Volume + volSnapshots []civogo.VolumeSnapshot + expectedError error + expectedResp *csi.CreateSnapshotResponse + }{ + { + name: "snapshot name missing", + req: &csi.CreateSnapshotRequest{ + Name: "snapshot-vol-1", + SourceVolumeId: "vol-1", + }, + sourceVolume: &civogo.Volume{ + ID: "vol-1", + }, + volSnapshots: []civogo.VolumeSnapshot{ + { + Name: "snapshot-vol-1", + VolumeID: "vol-x", + }, + }, + expectedError: status.Error(codes.AlreadyExists, "snapshot with the same name \"snapshot-vol-1\" but with different SourceVolumeId already exist"), + expectedResp: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fc, _ := civogo.NewFakeClient() + fc.Volumes = []civogo.Volume{*tt.sourceVolume} + fc.VolumeSnapshots = tt.volSnapshots + + + d, _ := driver.NewTestDriver(fc) + + resp, err := d.CreateSnapshot(context.Background(), tt.req) + + if tt.expectedError != nil { + assert.Nil(t, resp) + assert.Equal(t, tt.expectedError, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tt.expectedResp, resp) + } + }) + } + +} + +func TestListSnapshots(t *testing.T) { + tests := []struct { + name string + req *csi.ListSnapshotsRequest + snapshots []civogo.VolumeSnapshot + expectedError error + expectedResp *csi.ListSnapshotsResponse + }{ + { + name: "No snapshots found", + req: &csi.ListSnapshotsRequest{}, + snapshots: []civogo.VolumeSnapshot{}, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{}, + }, + }, + { + name: "starting token provided", + req: &csi.ListSnapshotsRequest{ + StartingToken: "12", + }, + snapshots: []civogo.VolumeSnapshot{}, + expectedError: status.Error(codes.Aborted, "starting-token not supported"), + expectedResp: nil, + }, + { + name: "Both snapshotID and sourceVolumeID given", + req: &csi.ListSnapshotsRequest{ + SnapshotId: "snap-1", + SourceVolumeId: "vol-1", + }, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-1", VolumeID: "vol-1"}, + }, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + {Snapshot: &csi.Snapshot{SnapshotId: "snap-1", SourceVolumeId: "vol-1"}}, + }, + }, + }, + { + name: "Only snapshotID given", + req: &csi.ListSnapshotsRequest{ + SnapshotId: "snap-1", + }, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-1", VolumeID: "vol-1"}, + }, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + {Snapshot: &csi.Snapshot{SnapshotId: "snap-1", SourceVolumeId: "vol-1"}}, + }, + }, + }, + { + name: "non-existing snapshotID given", + req: &csi.ListSnapshotsRequest{ + SnapshotId: "snap-2", + }, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-1", VolumeID: "vol-1"}, + }, + expectedResp: &csi.ListSnapshotsResponse{}, + }, + { + name: "Only sourceVolumeID having single snapshot given", + req: &csi.ListSnapshotsRequest{ + SourceVolumeId: "vol-1", + }, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-1", VolumeID: "vol-1"}, + }, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + {Snapshot: &csi.Snapshot{SnapshotId: "snap-1", SourceVolumeId: "vol-1"}}, + }, + }, + }, + { + name: "Only sourceVolumeID having multiple snapshots given", + req: &csi.ListSnapshotsRequest{ + SourceVolumeId: "vol-1", + }, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-1", VolumeID: "vol-1"}, + {SnapshotID: "snap-2", VolumeID: "vol-1"}, + {SnapshotID: "snap-3", VolumeID: "vol-1"}, + }, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + {Snapshot: &csi.Snapshot{SnapshotId: "snap-1", SourceVolumeId: "vol-1"}}, + {Snapshot: &csi.Snapshot{SnapshotId: "snap-2", SourceVolumeId: "vol-1"}}, + {Snapshot: &csi.Snapshot{SnapshotId: "snap-3", SourceVolumeId: "vol-1"}}, + }, + }, + }, + { + name: "Multiple snapshots found", + req: &csi.ListSnapshotsRequest{}, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-2", VolumeID: "vol-2"}, + {SnapshotID: "snap-1", VolumeID: "vol-1"}, + }, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + {Snapshot: &csi.Snapshot{SnapshotId: "snap-1", SourceVolumeId: "vol-1"}}, + {Snapshot: &csi.Snapshot{SnapshotId: "snap-2", SourceVolumeId: "vol-2"}}, + }, + }, + }, + { + name: "Empty snapshot creationTime", + req: &csi.ListSnapshotsRequest{}, + snapshots: []civogo.VolumeSnapshot{ + {SnapshotID: "snap-1", VolumeID: "vol-1", CreationTime: ""}, + }, + expectedResp: &csi.ListSnapshotsResponse{ + Entries: []*csi.ListSnapshotsResponse_Entry{ + {Snapshot: &csi.Snapshot{SnapshotId: "snap-1", SourceVolumeId: "vol-1"}}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fc, _ := civogo.NewFakeClient() + fc.VolumeSnapshots = tt.snapshots + + d, _ := driver.NewTestDriver(fc) + + resp, err := d.ListSnapshots(context.Background(), tt.req) + + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tt.expectedResp, resp) + } + }) + } +} + +func TestConvertSnapshot(t *testing.T) { + creationTime := "2024-02-12T10:00:00Z" + expectedTime, _ := driver.ParseTimeToProtoTimestamp(creationTime) + tests := []struct { + name string + input *civogo.VolumeSnapshot + expected *csi.ListSnapshotsResponse_Entry + expectedError bool + }{ + { + name: "Valid Snapshot Conversion", + input: &civogo.VolumeSnapshot{ + SnapshotID: "snap-123", + VolumeID: "vol-123", + SourceVolumeName: "vol1", + RestoreSize: 1024, + State: "Available", + CreationTime: creationTime, + }, + expected: &csi.ListSnapshotsResponse_Entry{ + Snapshot: &csi.Snapshot{ + SnapshotId: "snap-123", + SourceVolumeId: "vol-123", + SizeBytes: 1024, + ReadyToUse: true, + CreationTime: expectedTime, + }, + }, + expectedError: false, + }, + { + name: "Invalid Creation Time", + input: &civogo.VolumeSnapshot{ + SnapshotID: "snap-456", + VolumeID: "vol-456", + RestoreSize: 2048, + State: "creating", + CreationTime: "invalid-time", + }, + expected: nil, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := driver.ConvertSnapshot(tt.input) + + if (err != nil) != tt.expectedError { + t.Errorf("convertSnapshot() error = %v, expectedError %v", err, tt.expectedError) + } + + if err == nil && !assert.Equal(t, result, tt.expected) { + t.Errorf("Got:\n%v\n\n, expected:\n%v\n\n", result, tt.expected) + } + }) + } +}