Skip to content

Commit 25be424

Browse files
committed
Add disk image snapshot support
1 parent e0d0e51 commit 25be424

File tree

7 files changed

+684
-136
lines changed

7 files changed

+684
-136
lines changed

pkg/common/parameters.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ const (
3030

3131
// Parameters for VolumeSnapshotClass
3232
ParameterKeyStorageLocations = "storage-locations"
33-
34-
replicationTypeNone = "none"
33+
ParameterKeySnapshotType = "snapshot-type"
34+
ParameterKeyImageFamily = "image-family"
35+
DiskSnapshotType = "snapshots"
36+
DiskImageType = "images"
37+
replicationTypeNone = "none"
3538

3639
// Keys for PV and PVC parameters as reported by external-provisioner
3740
ParameterKeyPVCName = "csi.storage.k8s.io/pvc/name"
@@ -67,6 +70,8 @@ type DiskParameters struct {
6770
// SnapshotParameters contains normalized and defaulted parameters for snapshots
6871
type SnapshotParameters struct {
6972
StorageLocations []string
73+
SnapshotType string
74+
ImageFamily string
7075
}
7176

7277
// ExtractAndDefaultParameters will take the relevant parameters from a map and
@@ -131,6 +136,7 @@ func ExtractAndDefaultParameters(parameters map[string]string, driverName string
131136
func ExtractAndDefaultSnapshotParameters(parameters map[string]string) (SnapshotParameters, error) {
132137
p := SnapshotParameters{
133138
StorageLocations: []string{},
139+
SnapshotType: DiskSnapshotType,
134140
}
135141
for k, v := range parameters {
136142
switch strings.ToLower(k) {
@@ -140,6 +146,14 @@ func ExtractAndDefaultSnapshotParameters(parameters map[string]string) (Snapshot
140146
return p, err
141147
}
142148
p.StorageLocations = normalizedStorageLocations
149+
case ParameterKeySnapshotType:
150+
err := ValidateSnapshotType(v)
151+
if err != nil {
152+
return p, err
153+
}
154+
p.SnapshotType = v
155+
case ParameterKeyImageFamily:
156+
p.ImageFamily = v
143157
default:
144158
return p, fmt.Errorf("parameters contains invalid option %q", k)
145159
}

pkg/common/parameters_test.go

+20-7
Original file line numberDiff line numberDiff line change
@@ -165,37 +165,50 @@ func TestExtractAndDefaultParameters(t *testing.T) {
165165
}
166166
}
167167

168-
// Currently the only parameter is storage-locations, which is already tested
169-
// in utils_test/TestSnapshotStorageLocations. Here we just test the case where
170-
// no parameter is set in the snapshot class.
168+
// Currently the storage-locations parameter is tested in utils_test/TestSnapshotStorageLocations.
169+
// Here we just test other parameters.
171170
func TestSnapshotParameters(t *testing.T) {
172171
tests := []struct {
173172
desc string
174173
parameters map[string]string
175174
expectedSnapshotParames SnapshotParameters
175+
expectError bool
176176
}{
177177
{
178178
desc: "valid parameter",
179-
parameters: map[string]string{ParameterKeyStorageLocations: "ASIA "},
179+
parameters: map[string]string{ParameterKeyStorageLocations: "ASIA ", ParameterKeySnapshotType: "images", ParameterKeyImageFamily: "test-family"},
180180
expectedSnapshotParames: SnapshotParameters{
181181
StorageLocations: []string{"asia"},
182+
SnapshotType: DiskImageType,
183+
ImageFamily: "test-family",
182184
},
185+
expectError: false,
183186
},
184187
{
185188
desc: "nil parameter",
186189
parameters: nil,
187190
expectedSnapshotParames: SnapshotParameters{
188191
StorageLocations: []string{},
192+
SnapshotType: DiskSnapshotType,
189193
},
194+
expectError: false,
195+
},
196+
{
197+
desc: "invalid snapshot type",
198+
parameters: map[string]string{ParameterKeySnapshotType: "invalid-type"},
199+
expectError: true,
190200
},
191201
}
192202
for _, tc := range tests {
193203
t.Run(tc.desc, func(t *testing.T) {
194204
p, err := ExtractAndDefaultSnapshotParameters(tc.parameters)
195-
if err != nil {
196-
t.Errorf("Got error processing snapshot parameters: %v; expect no error", err)
205+
if err != nil && !tc.expectError {
206+
t.Errorf("Got error %v; expect no error", err)
207+
}
208+
if err == nil && tc.expectError {
209+
t.Error("Got no error; expect an error")
197210
}
198-
if !reflect.DeepEqual(p, tc.expectedSnapshotParames) {
211+
if err == nil && !reflect.DeepEqual(p, tc.expectedSnapshotParames) {
199212
t.Errorf("Got ExtractAndDefaultSnapshotParameters(%+v) = %+v; expect %+v", tc.parameters, p, tc.expectedSnapshotParames)
200213
}
201214
})

pkg/common/utils.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,15 @@ func GenerateUnderspecifiedVolumeID(diskName string, isZonal bool) string {
114114
return fmt.Sprintf(volIDRegionalFmt, UnspecifiedValue, UnspecifiedValue, diskName)
115115
}
116116

117-
func SnapshotIDToProjectKey(id string) (string, string, error) {
117+
func SnapshotIDToProjectKey(id string) (string, string, string, error) {
118118
splitId := strings.Split(id, "/")
119119
if len(splitId) != snapshotTotalElements {
120-
return "", "", fmt.Errorf("failed to get id components. Expected projects/{project}/global/snapshot/{name}. Got: %s", id)
120+
return "", "", "", fmt.Errorf("failed to get id components. Expected projects/{project}/global/{snapshots|images}/{name}. Got: %s", id)
121121
}
122122
if splitId[snapshotTopologyKey] == "global" {
123-
return splitId[snapshotProjectKey], splitId[snapshotTotalElements-1], nil
123+
return splitId[snapshotProjectKey], splitId[snapshotTotalElements-2], splitId[snapshotTotalElements-1], nil
124124
} else {
125-
return "", "", fmt.Errorf("could not get id components, expected global, got: %v", splitId[snapshotTopologyKey])
125+
return "", "", "", fmt.Errorf("could not get id components, expected global, got: %v", splitId[snapshotTopologyKey])
126126
}
127127
}
128128

@@ -238,3 +238,13 @@ func ProcessStorageLocations(storageLocations string) ([]string, error) {
238238
}
239239
return []string{normalizedLoc}, nil
240240
}
241+
242+
// ValidateSnapshotType validates the type
243+
func ValidateSnapshotType(snapshotType string) error {
244+
switch snapshotType {
245+
case DiskSnapshotType, DiskImageType:
246+
return nil
247+
default:
248+
return fmt.Errorf("invalid snapshot type %s", snapshotType)
249+
}
250+
}

pkg/gce-cloud-provider/compute/fake-gce.go

+85-41
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package gcecloudprovider
1717
import (
1818
"context"
1919
"fmt"
20-
"strconv"
2120
"strings"
2221

2322
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta"
@@ -37,6 +36,7 @@ const (
3736
Timestamp = "2018-09-05T15:17:08.270-07:00"
3837
BasePath = "https://www.googleapis.com/compute/v1/projects/"
3938
snapshotURITemplateGlobal = "%s/global/snapshots/%s" //{gce.projectID}/global/snapshots/{snapshot.Name}"
39+
imageURITemplateGlobal = "%s/global/images/%s" //{gce.projectID}/global/images/{image.Name}"
4040
)
4141

4242
type FakeCloudProvider struct {
@@ -47,6 +47,7 @@ type FakeCloudProvider struct {
4747
pageTokens map[string]sets.String
4848
instances map[string]*computev1.Instance
4949
snapshots map[string]*computev1.Snapshot
50+
images map[string]*computev1.Image
5051

5152
// marker to set disk status during InsertDisk operation.
5253
mockDiskStatus string
@@ -61,6 +62,7 @@ func CreateFakeCloudProvider(project, zone string, cloudDisks []*CloudDisk) (*Fa
6162
disks: map[string]*CloudDisk{},
6263
instances: map[string]*computev1.Instance{},
6364
snapshots: map[string]*computev1.Snapshot{},
65+
images: map[string]*computev1.Image{},
6466
pageTokens: map[string]sets.String{},
6567
// A newly created disk is marked READY by default.
6668
mockDiskStatus: "READY",
@@ -122,7 +124,7 @@ func (cloud *FakeCloudProvider) ListDisks(ctx context.Context) ([]*computev1.Dis
122124
return d, "", nil
123125
}
124126

125-
func (cloud *FakeCloudProvider) ListSnapshots(ctx context.Context, filter string, maxEntries int64, pageToken string) ([]*computev1.Snapshot, string, error) {
127+
func (cloud *FakeCloudProvider) ListSnapshots(ctx context.Context, filter string) ([]*computev1.Snapshot, string, error) {
126128
var sourceDisk string
127129
snapshots := []*computev1.Snapshot{}
128130
if len(filter) > 0 {
@@ -141,45 +143,7 @@ func (cloud *FakeCloudProvider) ListSnapshots(ctx context.Context, filter string
141143
snapshots = append(snapshots, snapshot)
142144
}
143145

144-
var (
145-
ulenSnapshots = len(snapshots)
146-
startingToken int
147-
)
148-
149-
if len(pageToken) > 0 {
150-
i, err := strconv.ParseUint(pageToken, 10, 32)
151-
if err != nil {
152-
return nil, "", invalidError()
153-
}
154-
startingToken = int(i)
155-
}
156-
157-
if startingToken > ulenSnapshots {
158-
return nil, "", invalidError()
159-
}
160-
161-
// Discern the number of remaining entries.
162-
rem := ulenSnapshots - startingToken
163-
164-
// If maxEntries is 0 or greater than the number of remaining entries then
165-
// set maxEntries to the number of remaining entries.
166-
max := int(maxEntries)
167-
if max == 0 || max > rem {
168-
max = rem
169-
}
170-
171-
results := []*computev1.Snapshot{}
172-
j := startingToken
173-
for i := 0; i < max; i++ {
174-
results = append(results, snapshots[j])
175-
j++
176-
}
177-
178-
var nextToken string
179-
if j < ulenSnapshots {
180-
nextToken = fmt.Sprintf("%d", j)
181-
}
182-
return results, nextToken, nil
146+
return snapshots, "", nil
183147
}
184148

185149
// Disk Methods
@@ -238,6 +202,7 @@ func (cloud *FakeCloudProvider) InsertDisk(ctx context.Context, project string,
238202
Type: cloud.GetDiskTypeURI(project, volKey, params.DiskType),
239203
SourceSnapshotId: snapshotID,
240204
SourceDiskId: volumeContentSourceVolumeID,
205+
SourceImageId: snapshotID,
241206
Status: cloud.mockDiskStatus,
242207
Labels: params.Labels,
243208
}
@@ -399,6 +364,71 @@ func (cloud *FakeCloudProvider) DeleteSnapshot(ctx context.Context, project, sna
399364
return nil
400365
}
401366

367+
func (cloud *FakeCloudProvider) ListImages(ctx context.Context, filter string) ([]*computev1.Image, string, error) {
368+
var sourceDisk string
369+
images := []*computev1.Image{}
370+
if len(filter) > 0 {
371+
filterSplits := strings.Fields(filter)
372+
if len(filterSplits) != 3 || filterSplits[0] != "sourceDisk" {
373+
return nil, "", invalidError()
374+
}
375+
sourceDisk = filterSplits[2]
376+
}
377+
for _, image := range cloud.images {
378+
if len(sourceDisk) > 0 {
379+
if image.SourceDisk == sourceDisk {
380+
continue
381+
}
382+
}
383+
images = append(images, image)
384+
}
385+
386+
return images, "", nil
387+
}
388+
389+
func (cloud *FakeCloudProvider) GetImage(ctx context.Context, project, imageName string) (*computev1.Image, error) {
390+
image, ok := cloud.images[imageName]
391+
if !ok {
392+
return nil, notFoundError()
393+
}
394+
image.Status = "READY"
395+
return image, nil
396+
}
397+
398+
func (cloud *FakeCloudProvider) CreateImage(ctx context.Context, project string, volKey *meta.Key, imageName string, snapshotParams common.SnapshotParameters) (*computev1.Image, error) {
399+
if image, ok := cloud.images[imageName]; ok {
400+
return image, nil
401+
}
402+
403+
imageToCreate := &computev1.Image{
404+
CreationTimestamp: Timestamp,
405+
DiskSizeGb: int64(DiskSizeGb),
406+
Family: snapshotParams.ImageFamily,
407+
Name: imageName,
408+
SelfLink: cloud.getGlobalImageURI(project, imageName),
409+
SourceType: "RAW",
410+
Status: "PENDING",
411+
StorageLocations: snapshotParams.StorageLocations,
412+
}
413+
414+
switch volKey.Type() {
415+
case meta.Zonal:
416+
imageToCreate.SourceDisk = cloud.getZonalDiskSourceURI(project, volKey.Name, volKey.Zone)
417+
case meta.Regional:
418+
imageToCreate.SourceDisk = cloud.getRegionalDiskSourceURI(project, volKey.Name, volKey.Region)
419+
default:
420+
return nil, fmt.Errorf("could not create image, disk key was neither zonal nor regional, instead got: %v", volKey.String())
421+
}
422+
423+
cloud.images[imageName] = imageToCreate
424+
return imageToCreate, nil
425+
}
426+
427+
func (cloud *FakeCloudProvider) DeleteImage(ctx context.Context, project, imageName string) error {
428+
delete(cloud.images, imageName)
429+
return nil
430+
}
431+
402432
func (cloud *FakeCloudProvider) ValidateExistingSnapshot(resp *computev1.Snapshot, volKey *meta.Key) error {
403433
if resp == nil {
404434
return fmt.Errorf("disk does not exist")
@@ -447,6 +477,13 @@ func (cloud *FakeCloudProvider) getGlobalSnapshotURI(project, snapshotName strin
447477
snapshotName)
448478
}
449479

480+
func (cloud *FakeCloudProvider) getGlobalImageURI(project, imageName string) string {
481+
return BasePath + fmt.Sprintf(
482+
imageURITemplateGlobal,
483+
project,
484+
imageName)
485+
}
486+
450487
func (cloud *FakeCloudProvider) UpdateDiskStatus(s string) {
451488
cloud.mockDiskStatus = s
452489
}
@@ -467,6 +504,13 @@ func (cloud *FakeBlockingCloudProvider) CreateSnapshot(ctx context.Context, proj
467504
return cloud.FakeCloudProvider.CreateSnapshot(ctx, project, volKey, snapshotName, snapshotParams)
468505
}
469506

507+
func (cloud *FakeBlockingCloudProvider) CreateImage(ctx context.Context, project string, volKey *meta.Key, imageName string, snapshotParams common.SnapshotParameters) (*computev1.Image, error) {
508+
executeCreateSnapshot := make(chan struct{})
509+
cloud.ReadyToExecute <- executeCreateSnapshot
510+
<-executeCreateSnapshot
511+
return cloud.FakeCloudProvider.CreateImage(ctx, project, volKey, imageName, snapshotParams)
512+
}
513+
470514
func notFoundError() *googleapi.Error {
471515
return &googleapi.Error{
472516
Errors: []googleapi.ErrorItem{

0 commit comments

Comments
 (0)