Skip to content

Commit ddd6c68

Browse files
committed
Add GroupControllerServer interface for VolumeGroupSnapshot API
1 parent 1a6269a commit ddd6c68

File tree

3 files changed

+300
-1
lines changed

3 files changed

+300
-1
lines changed

pkg/hostpath/groupcontrollerserver.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hostpath
18+
19+
import (
20+
"os"
21+
22+
"github.com/container-storage-interface/spec/lib/go/csi"
23+
"github.com/golang/glog"
24+
"github.com/golang/protobuf/ptypes"
25+
"github.com/pborman/uuid"
26+
"golang.org/x/net/context"
27+
"google.golang.org/grpc/codes"
28+
"google.golang.org/grpc/status"
29+
30+
"github.com/kubernetes-csi/csi-driver-host-path/pkg/state"
31+
)
32+
33+
func (hp *hostPath) GroupControllerGetCapabilities(context.Context, *csi.GroupControllerGetCapabilitiesRequest) (*csi.GroupControllerGetCapabilitiesResponse, error) {
34+
return &csi.GroupControllerGetCapabilitiesResponse{
35+
Capabilities: []*csi.GroupControllerServiceCapability{{
36+
Type: &csi.GroupControllerServiceCapability_Rpc{
37+
Rpc: &csi.GroupControllerServiceCapability_RPC{
38+
Type: csi.GroupControllerServiceCapability_RPC_CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT,
39+
},
40+
},
41+
}},
42+
}, nil
43+
}
44+
45+
func (hp *hostPath) CreateVolumeGroupSnapshot(ctx context.Context, req *csi.CreateVolumeGroupSnapshotRequest) (*csi.CreateVolumeGroupSnapshotResponse, error) {
46+
if err := hp.validateGroupControllerServiceRequest(csi.GroupControllerServiceCapability_RPC_CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT); err != nil {
47+
glog.V(3).Infof("invalid create volume group snapshot req: %v", req)
48+
return nil, err
49+
}
50+
51+
if len(req.GetName()) == 0 {
52+
return nil, status.Error(codes.InvalidArgument, "Name missing in request")
53+
}
54+
// Check arguments
55+
if len(req.GetSourceVolumeIds()) == 0 {
56+
return nil, status.Error(codes.InvalidArgument, "SourceVolumeIds missing in request")
57+
}
58+
59+
// Lock before acting on global state. A production-quality
60+
// driver might use more fine-grained locking.
61+
hp.mutex.Lock()
62+
defer hp.mutex.Unlock()
63+
64+
// Need to check for already existing groupsnapshot name, and if found check for the
65+
// requested sourceVolumeIds and sourceVolumeIds of groupsnapshot that has been created.
66+
if exGS, err := hp.state.GetGroupSnapshotByName(req.GetName()); err == nil {
67+
// Since err is nil, it means the groupsnapshot with the same name already exists. Need
68+
// to check if the sourceVolumeIds of existing groupsnapshot is the same as in new request.
69+
70+
if !exGS.MatchesSourceVolumeIDs(req.GetSourceVolumeIds()) {
71+
return nil, status.Errorf(codes.AlreadyExists, "group snapshot with the same name: %s but with different SourceVolumeIds already exist", req.GetName())
72+
}
73+
74+
// same groupsnapshot has been created.
75+
snapshots := make([]*csi.Snapshot, len(exGS.SnapshotIDs))
76+
readyToUse := true
77+
78+
for i, snapshotID := range exGS.SnapshotIDs {
79+
snapshot, err := hp.state.GetSnapshotByID(snapshotID)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
snapshots[i] = &csi.Snapshot{
85+
SizeBytes: snapshot.SizeBytes,
86+
CreationTime: snapshot.CreationTime,
87+
ReadyToUse: snapshot.ReadyToUse,
88+
GroupSnapshotId: snapshot.GroupSnapshotID,
89+
}
90+
91+
readyToUse = readyToUse && snapshot.ReadyToUse
92+
}
93+
94+
return &csi.CreateVolumeGroupSnapshotResponse{
95+
GroupSnapshot: &csi.VolumeGroupSnapshot{
96+
GroupSnapshotId: exGS.Id,
97+
Snapshots: snapshots,
98+
CreationTime: exGS.CreationTime,
99+
ReadyToUse: readyToUse,
100+
},
101+
}, nil
102+
}
103+
104+
groupSnapshot := state.GroupSnapshot{
105+
Name: req.GetName(),
106+
Id: uuid.NewUUID().String(),
107+
CreationTime: ptypes.TimestampNow(),
108+
SnapshotIDs: make([]string, len(req.GetSourceVolumeIds())),
109+
ReadyToUse: true,
110+
}
111+
112+
snapshots := make([]*csi.Snapshot, len(req.GetSourceVolumeIds()))
113+
114+
// TODO: defer a cleanup function to remove snapshots in case of a failure
115+
116+
for i, volumeID := range req.GetSourceVolumeIds() {
117+
hostPathVolume, err := hp.state.GetVolumeByID(volumeID)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
snapshotID := uuid.NewUUID().String()
123+
file := hp.getSnapshotPath(snapshotID)
124+
125+
if err := hp.createSnapshotFromVolume(hostPathVolume, file); err != nil {
126+
return nil, err
127+
}
128+
129+
glog.V(4).Infof("create volume snapshot %s", file)
130+
snapshot := state.Snapshot{}
131+
snapshot.Name = req.GetName() + "-" + volumeID
132+
snapshot.Id = snapshotID
133+
snapshot.VolID = volumeID
134+
snapshot.Path = file
135+
snapshot.CreationTime = groupSnapshot.CreationTime
136+
snapshot.SizeBytes = hostPathVolume.VolSize
137+
snapshot.ReadyToUse = true
138+
snapshot.GroupSnapshotID = groupSnapshot.Id
139+
140+
hp.state.UpdateSnapshot(snapshot)
141+
142+
groupSnapshot.SnapshotIDs[i] = snapshotID
143+
144+
snapshots[i] = &csi.Snapshot{
145+
SizeBytes: hostPathVolume.VolSize,
146+
SnapshotId: snapshotID,
147+
SourceVolumeId: volumeID,
148+
CreationTime: groupSnapshot.CreationTime,
149+
ReadyToUse: true,
150+
GroupSnapshotId: groupSnapshot.Id,
151+
}
152+
}
153+
154+
if err := hp.state.UpdateGroupSnapshot(groupSnapshot); err != nil {
155+
return nil, err
156+
}
157+
158+
return &csi.CreateVolumeGroupSnapshotResponse{
159+
GroupSnapshot: &csi.VolumeGroupSnapshot{
160+
GroupSnapshotId: groupSnapshot.Id,
161+
Snapshots: snapshots,
162+
CreationTime: groupSnapshot.CreationTime,
163+
ReadyToUse: groupSnapshot.ReadyToUse,
164+
},
165+
}, nil
166+
}
167+
168+
func (hp *hostPath) DeleteVolumeGroupSnapshot(ctx context.Context, req *csi.DeleteVolumeGroupSnapshotRequest) (*csi.DeleteVolumeGroupSnapshotResponse, error) {
169+
if err := hp.validateGroupControllerServiceRequest(csi.GroupControllerServiceCapability_RPC_CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT); err != nil {
170+
glog.V(3).Infof("invalid delete volume group snapshot req: %v", req)
171+
return nil, err
172+
}
173+
174+
// Check arguments
175+
if len(req.GetGroupSnapshotId()) == 0 {
176+
return nil, status.Error(codes.InvalidArgument, "GroupSnapshot ID missing in request")
177+
}
178+
179+
groupSnapshotID := req.GetGroupSnapshotId()
180+
181+
// Lock before acting on global state. A production-quality
182+
// driver might use more fine-grained locking.
183+
hp.mutex.Lock()
184+
defer hp.mutex.Unlock()
185+
186+
groupSnapshot, err := hp.state.GetGroupSnapshotByID(groupSnapshotID)
187+
if err != nil {
188+
// ok if NotFound, the VolumeGroupSnapshot was deleted already
189+
if status.Code(err) == codes.NotFound {
190+
return &csi.DeleteVolumeGroupSnapshotResponse{}, nil
191+
}
192+
193+
return nil, err
194+
}
195+
196+
for _, snapshotID := range groupSnapshot.SnapshotIDs {
197+
glog.V(4).Infof("deleting snapshot %s", snapshotID)
198+
path := hp.getSnapshotPath(snapshotID)
199+
os.RemoveAll(path)
200+
201+
if err := hp.state.DeleteSnapshot(snapshotID); err != nil {
202+
return nil, err
203+
}
204+
}
205+
206+
glog.V(4).Infof("deleting groupsnapshot %s", groupSnapshotID)
207+
if err := hp.state.DeleteGroupSnapshot(groupSnapshotID); err != nil {
208+
return nil, err
209+
}
210+
211+
return &csi.DeleteVolumeGroupSnapshotResponse{}, nil
212+
}
213+
214+
func (hp *hostPath) GetVolumeGroupSnapshot(ctx context.Context, req *csi.GetVolumeGroupSnapshotRequest) (*csi.GetVolumeGroupSnapshotResponse, error) {
215+
if err := hp.validateGroupControllerServiceRequest(csi.GroupControllerServiceCapability_RPC_CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT); err != nil {
216+
glog.V(3).Infof("invalid get volume group snapshot req: %v", req)
217+
return nil, err
218+
}
219+
220+
groupSnapshotID := req.GetGroupSnapshotId()
221+
222+
// Check arguments
223+
if len(groupSnapshotID) == 0 {
224+
return nil, status.Error(codes.InvalidArgument, "GroupSnapshot ID missing in request")
225+
}
226+
227+
// Lock before acting on global state. A production-quality
228+
// driver might use more fine-grained locking.
229+
hp.mutex.Lock()
230+
defer hp.mutex.Unlock()
231+
232+
groupSnapshot, err := hp.state.GetGroupSnapshotByID(groupSnapshotID)
233+
if err != nil {
234+
return nil, err
235+
}
236+
237+
if !groupSnapshot.MatchesSourceVolumeIDs(req.GetSnapshotIds()) {
238+
return nil, status.Error(codes.InvalidArgument, "Snapshot IDs do not match the GroupSnapshot IDs")
239+
}
240+
241+
snapshots := make([]*csi.Snapshot, len(groupSnapshot.SnapshotIDs))
242+
for i, snapshotID := range groupSnapshot.SnapshotIDs {
243+
snapshot, err := hp.state.GetSnapshotByID(snapshotID)
244+
if err != nil {
245+
return nil, err
246+
}
247+
248+
snapshots[i] = &csi.Snapshot{
249+
SizeBytes: snapshot.SizeBytes,
250+
SnapshotId: snapshotID,
251+
SourceVolumeId: snapshot.VolID,
252+
CreationTime: snapshot.CreationTime,
253+
ReadyToUse: snapshot.ReadyToUse,
254+
GroupSnapshotId: snapshot.GroupSnapshotID,
255+
}
256+
}
257+
258+
return &csi.GetVolumeGroupSnapshotResponse{
259+
GroupSnapshot: &csi.VolumeGroupSnapshot{
260+
GroupSnapshotId: groupSnapshotID,
261+
Snapshots: snapshots,
262+
CreationTime: groupSnapshot.CreationTime,
263+
ReadyToUse: groupSnapshot.ReadyToUse,
264+
},
265+
}, nil
266+
}
267+
268+
func (hp *hostPath) validateGroupControllerServiceRequest(c csi.GroupControllerServiceCapability_RPC_Type) error {
269+
if c == csi.GroupControllerServiceCapability_RPC_UNKNOWN {
270+
return nil
271+
}
272+
273+
if c == csi.GroupControllerServiceCapability_RPC_CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT {
274+
return nil
275+
}
276+
277+
return status.Errorf(codes.InvalidArgument, "unsupported capability %s", c)
278+
}

pkg/state/state.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"encoding/json"
2323
"errors"
2424
"os"
25+
"sort"
2526

2627
timestamp "github.com/golang/protobuf/ptypes/timestamp"
2728
"google.golang.org/grpc/codes"
@@ -334,3 +335,23 @@ func (s *state) DeleteGroupSnapshot(groupSnapshotID string) error {
334335
}
335336
return nil
336337
}
338+
339+
func (gs *GroupSnapshot) MatchesSourceVolumeIDs(sourceVolumeIDs []string) bool {
340+
snapshotIDs := gs.SnapshotIDs
341+
342+
if len(snapshotIDs) != len(sourceVolumeIDs) {
343+
return false
344+
}
345+
346+
// sort slices so that values are at the same location
347+
sort.Strings(snapshotIDs)
348+
sort.Strings(sourceVolumeIDs)
349+
350+
for i, v := range snapshotIDs {
351+
if v != sourceVolumeIDs[i] {
352+
return false
353+
}
354+
}
355+
356+
return true
357+
}

pkg/state/state_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func TestVolumeGroupSnapshots(t *testing.T) {
134134
require.Empty(t, s.GetGroupSnapshots(), "initial groupsnapshots")
135135

136136
_, err = s.GetGroupSnapshotByID("foo")
137-
require.Equal(t, codes.NotFound, status.Convert(err).Code(), "GetSnapshotByID of non-existent groupsnapshot")
137+
require.Equal(t, codes.NotFound, status.Convert(err).Code(), "GetGroupSnapshotByID of non-existent groupsnapshot")
138138
require.Contains(t, status.Convert(err).Message(), "foo")
139139

140140
err = s.UpdateGroupSnapshot(GroupSnapshot{Id: "foo", Name: "bar"})

0 commit comments

Comments
 (0)