Skip to content

Commit

Permalink
Merge pull request #286 from zacharya/list-snapshot
Browse files Browse the repository at this point in the history
Implementing ListSnapshots
  • Loading branch information
k8s-ci-robot authored May 11, 2019
2 parents 6a79b78 + 6c0eb0d commit 67f791d
Show file tree
Hide file tree
Showing 6 changed files with 615 additions and 10 deletions.
88 changes: 86 additions & 2 deletions pkg/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ var (

// ErrAlreadyExists is returned when a resource is already existent.
ErrAlreadyExists = errors.New("Resource already exists")

// ErrMultiSnapshots is returned when multiple snapshots are found
// with the same ID
ErrMultiSnapshots = errors.New("Multiple snapshots with the same name found")

// ErrInvalidMaxResults is returned when a MaxResults pagination parameter is between 1 and 4
ErrInvalidMaxResults = errors.New("MaxResults parameter must be 0 or greater than or equal to 5")
)

// Disk represents a EBS volume
Expand Down Expand Up @@ -124,11 +131,23 @@ type Snapshot struct {
ReadyToUse bool
}

// ListSnapshotsResponse is the container for our snapshots along with a pagination token to pass back to the caller
type ListSnapshotsResponse struct {
Snapshots []*Snapshot
NextToken string
}

// SnapshotOptions represents parameters to create an EBS volume
type SnapshotOptions struct {
Tags map[string]string
}

// ec2ListSnapshotsResponse is a helper struct returned from the AWS API calling function to the main ListSnapshots function
type ec2ListSnapshotsResponse struct {
Snapshots []*ec2.Snapshot
NextToken *string
}

// EC2 abstracts aws.EC2 to facilitate its mocking.
// See https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/ for details
type EC2 interface {
Expand Down Expand Up @@ -156,6 +175,7 @@ type Cloud interface {
CreateSnapshot(ctx context.Context, volumeID string, snapshotOptions *SnapshotOptions) (snapshot *Snapshot, err error)
DeleteSnapshot(ctx context.Context, snapshotID string) (success bool, err error)
GetSnapshotByName(ctx context.Context, name string) (snapshot *Snapshot, err error)
ListSnapshots(ctx context.Context, volumeID string, maxResults int64, nextToken string) (listSnapshotsResponse *ListSnapshotsResponse, err error)
}

type cloud struct {
Expand Down Expand Up @@ -545,6 +565,49 @@ func (c *cloud) GetSnapshotByName(ctx context.Context, name string) (snapshot *S
return c.ec2SnapshotResponseToStruct(ec2snapshot), nil
}

// ListSnapshots retrieves AWS EBS snapshots for an optionally specified volume ID. If maxResults is set, it will return up to maxResults snapshots. If there are more snapshots than maxResults,
// a next token value will be returned to the client as well. They can use this token with subsequent calls to retrieve the next page of results. If maxResults is not set (0),
// there will be no restriction up to 1000 results (https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeSnapshotsInput).
func (c *cloud) ListSnapshots(ctx context.Context, volumeID string, maxResults int64, nextToken string) (listSnapshotsResponse *ListSnapshotsResponse, err error) {
if maxResults > 0 && maxResults < 5 {
return nil, ErrInvalidMaxResults
}

describeSnapshotsInput := &ec2.DescribeSnapshotsInput{
MaxResults: aws.Int64(maxResults),
}

if len(nextToken) != 0 {
describeSnapshotsInput.NextToken = aws.String(nextToken)
}
if len(volumeID) != 0 {
describeSnapshotsInput.Filters = []*ec2.Filter{
{
Name: aws.String("volume-id"),
Values: []*string{aws.String(volumeID)},
},
}
}

ec2SnapshotsResponse, err := c.listSnapshots(ctx, describeSnapshotsInput)
if err != nil {
return nil, err
}
var snapshots []*Snapshot
for _, ec2Snapshot := range ec2SnapshotsResponse.Snapshots {
snapshots = append(snapshots, c.ec2SnapshotResponseToStruct(ec2Snapshot))
}

if len(snapshots) == 0 {
return nil, ErrNotFound
}

return &ListSnapshotsResponse{
Snapshots: snapshots,
NextToken: aws.StringValue(ec2SnapshotsResponse.NextToken),
}, nil
}

// Helper method converting EC2 snapshot type to the internal struct
func (c *cloud) ec2SnapshotResponseToStruct(ec2Snapshot *ec2.Snapshot) *Snapshot {
if ec2Snapshot == nil {
Expand Down Expand Up @@ -628,7 +691,6 @@ func (c *cloud) getInstance(ctx context.Context, nodeID string) (*ec2.Instance,
func (c *cloud) getSnapshot(ctx context.Context, request *ec2.DescribeSnapshotsInput) (*ec2.Snapshot, error) {
var snapshots []*ec2.Snapshot
var nextToken *string

for {
response, err := c.ec2.DescribeSnapshotsWithContext(ctx, request)
if err != nil {
Expand All @@ -643,14 +705,36 @@ func (c *cloud) getSnapshot(ctx context.Context, request *ec2.DescribeSnapshotsI
}

if l := len(snapshots); l > 1 {
return nil, errors.New("Multiple snapshots with the same name found")
return nil, ErrMultiSnapshots
} else if l < 1 {
return nil, ErrNotFound
}

return snapshots[0], nil
}

// listSnapshots returns all snapshots based from a request
func (c *cloud) listSnapshots(ctx context.Context, request *ec2.DescribeSnapshotsInput) (*ec2ListSnapshotsResponse, error) {
var snapshots []*ec2.Snapshot
var nextToken *string

response, err := c.ec2.DescribeSnapshotsWithContext(ctx, request)
if err != nil {
return nil, err
}

snapshots = append(snapshots, response.Snapshots...)

if response.NextToken != nil {
nextToken = response.NextToken
}

return &ec2ListSnapshotsResponse{
Snapshots: snapshots,
NextToken: nextToken,
}, nil
}

// waitForVolume waits for volume to be in the "available" state.
// On a random AWS account (shared among several developers) it took 4s on average.
func (c *cloud) waitForVolume(ctx context.Context, volumeID string) error {
Expand Down
212 changes: 212 additions & 0 deletions pkg/cloud/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cloud

import (
"context"
"errors"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -647,6 +648,217 @@ func TestGetSnapshotByName(t *testing.T) {
}
}

func TestListSnapshots(t *testing.T) {
testCases := []struct {
name string
testFunc func(t *testing.T)
}{
{
name: "success: normal",
testFunc: func(t *testing.T) {
expSnapshots := []*Snapshot{
{
SourceVolumeID: "snap-test-volume1",
SnapshotID: "snap-test-name1",
},
{
SourceVolumeID: "snap-test-volume2",
SnapshotID: "snap-test-name2",
},
}
ec2Snapshots := []*ec2.Snapshot{
{
SnapshotId: aws.String(expSnapshots[0].SnapshotID),
VolumeId: aws.String("snap-test-volume1"),
State: aws.String("completed"),
},
{
SnapshotId: aws.String(expSnapshots[1].SnapshotID),
VolumeId: aws.String("snap-test-volume2"),
State: aws.String("completed"),
},
}

mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockEC2 := mocks.NewMockEC2(mockCtl)
c := newCloud(mockEC2)

ctx := context.Background()

mockEC2.EXPECT().DescribeSnapshotsWithContext(gomock.Eq(ctx), gomock.Any()).Return(&ec2.DescribeSnapshotsOutput{Snapshots: ec2Snapshots}, nil)

_, err := c.ListSnapshots(ctx, "", 0, "")
if err != nil {
t.Fatalf("ListSnapshots() failed: expected no error, got: %v", err)
}
},
},
{
name: "success: with volume ID",
testFunc: func(t *testing.T) {
sourceVolumeID := "snap-test-volume"
expSnapshots := []*Snapshot{
{
SourceVolumeID: sourceVolumeID,
SnapshotID: "snap-test-name1",
},
{
SourceVolumeID: sourceVolumeID,
SnapshotID: "snap-test-name2",
},
}
ec2Snapshots := []*ec2.Snapshot{
{
SnapshotId: aws.String(expSnapshots[0].SnapshotID),
VolumeId: aws.String(sourceVolumeID),
State: aws.String("completed"),
},
{
SnapshotId: aws.String(expSnapshots[1].SnapshotID),
VolumeId: aws.String(sourceVolumeID),
State: aws.String("completed"),
},
}

mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockEC2 := mocks.NewMockEC2(mockCtl)
c := newCloud(mockEC2)

ctx := context.Background()

mockEC2.EXPECT().DescribeSnapshotsWithContext(gomock.Eq(ctx), gomock.Any()).Return(&ec2.DescribeSnapshotsOutput{Snapshots: ec2Snapshots}, nil)

resp, err := c.ListSnapshots(ctx, sourceVolumeID, 0, "")
if err != nil {
t.Fatalf("ListSnapshots() failed: expected no error, got: %v", err)
}

if len(resp.Snapshots) != len(expSnapshots) {
t.Fatalf("Expected %d snapshots, got %d", len(expSnapshots), len(resp.Snapshots))
}

for _, snap := range resp.Snapshots {
if snap.SourceVolumeID != sourceVolumeID {
t.Fatalf("Unexpected source volume. Expected %s, got %s", sourceVolumeID, snap.SourceVolumeID)
}
}
},
},
{
name: "success: max results, next token",
testFunc: func(t *testing.T) {
maxResults := 5
nextTokenValue := "nextTokenValue"
var expSnapshots []*Snapshot
for i := 0; i < maxResults*2; i++ {
expSnapshots = append(expSnapshots, &Snapshot{
SourceVolumeID: "snap-test-volume1",
SnapshotID: fmt.Sprintf("snap-test-name%d", i),
})
}

var ec2Snapshots []*ec2.Snapshot
for i := 0; i < maxResults*2; i++ {
ec2Snapshots = append(ec2Snapshots, &ec2.Snapshot{
SnapshotId: aws.String(expSnapshots[i].SnapshotID),
VolumeId: aws.String(fmt.Sprintf("snap-test-volume%d", i)),
State: aws.String("completed"),
})
}

mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockEC2 := mocks.NewMockEC2(mockCtl)
c := newCloud(mockEC2)

ctx := context.Background()

firstCall := mockEC2.EXPECT().DescribeSnapshotsWithContext(gomock.Eq(ctx), gomock.Any()).Return(&ec2.DescribeSnapshotsOutput{
Snapshots: ec2Snapshots[:maxResults],
NextToken: aws.String(nextTokenValue),
}, nil)
secondCall := mockEC2.EXPECT().DescribeSnapshotsWithContext(gomock.Eq(ctx), gomock.Any()).Return(&ec2.DescribeSnapshotsOutput{
Snapshots: ec2Snapshots[maxResults:],
}, nil)
gomock.InOrder(
firstCall,
secondCall,
)

firstSnapshotsResponse, err := c.ListSnapshots(ctx, "", 5, "")
if err != nil {
t.Fatalf("ListSnapshots() failed: expected no error, got: %v", err)
}

if len(firstSnapshotsResponse.Snapshots) != maxResults {
t.Fatalf("Expected %d snapshots, got %d", maxResults, len(firstSnapshotsResponse.Snapshots))
}

if firstSnapshotsResponse.NextToken != nextTokenValue {
t.Fatalf("Expected next token value '%s' got '%s'", nextTokenValue, firstSnapshotsResponse.NextToken)
}

secondSnapshotsResponse, err := c.ListSnapshots(ctx, "", 0, firstSnapshotsResponse.NextToken)
if err != nil {
t.Fatalf("CreateSnapshot() failed: expected no error, got: %v", err)
}

if len(secondSnapshotsResponse.Snapshots) != maxResults {
t.Fatalf("Expected %d snapshots, got %d", maxResults, len(secondSnapshotsResponse.Snapshots))
}

if secondSnapshotsResponse.NextToken != "" {
t.Fatalf("Expected next token value to be empty got %s", secondSnapshotsResponse.NextToken)
}
},
},
{
name: "fail: AWS DescribeSnapshotsWithContext error",
testFunc: func(t *testing.T) {
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockEC2 := mocks.NewMockEC2(mockCtl)
c := newCloud(mockEC2)

ctx := context.Background()

mockEC2.EXPECT().DescribeSnapshotsWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, errors.New("test error"))

if _, err := c.ListSnapshots(ctx, "", 0, ""); err == nil {
t.Fatalf("ListSnapshots() failed: expected an error, got none")
}
},
},
{
name: "fail: no snapshots ErrNotFound",
testFunc: func(t *testing.T) {
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockEC2 := mocks.NewMockEC2(mockCtl)
c := newCloud(mockEC2)

ctx := context.Background()

mockEC2.EXPECT().DescribeSnapshotsWithContext(gomock.Eq(ctx), gomock.Any()).Return(&ec2.DescribeSnapshotsOutput{}, nil)

if _, err := c.ListSnapshots(ctx, "", 0, ""); err != nil {
if err != ErrNotFound {
t.Fatalf("Expected error %v, got %v", ErrNotFound, err)
}
} else {
t.Fatalf("Expected error, got none")
}
},
},
}

for _, tc := range testCases {
t.Run(tc.name, tc.testFunc)
}
}

func newCloud(mockEC2 EC2) Cloud {
return &cloud{
metadata: &Metadata{
Expand Down
Loading

0 comments on commit 67f791d

Please sign in to comment.