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
41 changes: 40 additions & 1 deletion docs/snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
| Parameter | Description of value |
|------------------------------------|-----------------------------------------------------------|
| fastSnapshotRestoreAvailabilityZones | Comma separated list of availability zones |
| outpostArn | Arn of the outpost you wish to have the snapshot saved to |
| outpostArn | Arn of the outpost you wish to have the snapshot saved to |
| snapshotLockMode | Lock mode (governance/compliance) |
| snapshotLockDuration | Lock duration in days |
| snapshotLockExpirationDate | Lock expiration date (RFC3339 format) |
| snapshotLockCoolOffPeriod | Cool-off period in hours (compliance mode only) |

The AWS EBS CSI Driver supports [tagging](tagging.md) through `VolumeSnapshotClass.parameters` (in v1.6.0 and later).
## Prerequisites
Expand Down Expand Up @@ -44,6 +48,41 @@ parameters:

The driver will attempt to check if the availability zones provided are supported for fast snapshot restore before attempting to create the snapshot. If the `EnableFastSnapshotRestores` API call fails, the driver will hard-fail the request and delete the snapshot. This is to ensure that the snapshot is not left in an inconsistent state.

# Snapshot Lock

The EBS CSI Driver provides support for [EBS Snapshot Lock](https://docs.aws.amazon.com/ebs/latest/userguide/ebs-snapshot-lock.html) via `VolumeSnapshotClass.parameters`. Snapshot locking protects snapshots from accidental or malicious deletion. A locked snapshot can't be deleted.

**Example - Lock in Governance Mode with Specified Duration**
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-aws-vsc-locked
driver: ebs.csi.aws.com
deletionPolicy: Delete
parameters:
snapshotLockMode: "governance"
snapshotLockDuration: "7"
```

**Example - Lock in Compliance Mode with Expiration Date and Cool Off Period**
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-aws-vsc-compliance
driver: ebs.csi.aws.com
deletionPolicy: Delete
parameters:
snapshotLockMode: "compliance"
snapshotLockExpirationDate: "2030-12-31T23:59:59Z"
snapshotLockCoolOffPeriod: "24"
```

## Failure Mode

If the `LockSnapshot` API call fails, the driver will hard-fail the request and delete the snapshot. This ensures that the snapshot is not left in an unlocked state when locking was explicitly requested.


# Amazon EBS Local Snapshots on Outposts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ kind: VolumeSnapshotClass
metadata:
name: csi-aws-vsc
driver: ebs.csi.aws.com
deletionPolicy: Delete
deletionPolicy: Delete
3 changes: 2 additions & 1 deletion hack/e2e/kops/patch-cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ spec:
"Effect": "Allow",
"Action": [
"ec2:CreateVolume",
"ec2:EnableFastSnapshotRestores"
"ec2:EnableFastSnapshotRestores",
"ec2:LockSnapshot"
],
"Resource": "arn:aws:ec2:*:*:snapshot/*"
},
Expand Down
16 changes: 15 additions & 1 deletion pkg/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,17 @@ type ListSnapshotsResponse struct {
NextToken string
}

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

// SnapshotLockOptions represents the snapshot lock specific parameters for locking en EBS snapshot.
type SnapshotLockOptions struct {
LockSnapshotInput ec2.LockSnapshotInput
}

// ec2ListSnapshotsResponse is a helper struct returned from the AWS API calling function to the main ListSnapshots function.
type ec2ListSnapshotsResponse struct {
Snapshots []types.Snapshot
Expand Down Expand Up @@ -1872,6 +1877,15 @@ func (c *cloud) CreateSnapshot(ctx context.Context, volumeID string, snapshotOpt
}, nil
}

func (c *cloud) LockSnapshot(ctx context.Context, lockSnapshotInput ec2.LockSnapshotInput) (*ec2.LockSnapshotOutput, error) {
klog.InfoS("Attempting to lock Snapshot", "request parameters: ", lockSnapshotInput)
response, err := c.ec2.LockSnapshot(ctx, &lockSnapshotInput)
if err != nil {
return nil, err
}
return response, nil
}

func (c *cloud) DeleteSnapshot(ctx context.Context, snapshotID string) (success bool, err error) {
request := &ec2.DeleteSnapshotInput{}
request.SnapshotId = aws.String(snapshotID)
Expand Down
53 changes: 53 additions & 0 deletions pkg/cloud/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5320,3 +5320,56 @@ func TestCheckIfIopsIncreaseOnExpansion(t *testing.T) {
})
}
}

func TestLockSnapshot(t *testing.T) {
testCases := []struct {
name string
input ec2.LockSnapshotInput
mockError error
expectErr bool
}{
{
name: "success: API call succeeds",
input: ec2.LockSnapshotInput{
SnapshotId: aws.String("snap-test-id"),
LockMode: types.LockModeGovernance,
LockDuration: aws.Int32(1),
},
mockError: nil,
expectErr: false,
},
{
name: "fail: AWS API error is propagated",
input: ec2.LockSnapshotInput{
SnapshotId: aws.String("snap-test-id"),
},
mockError: errors.New("InvalidSnapshot.NotFound"),
expectErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
mockEC2 := NewMockEC2API(mockCtrl)
c := newCloud(mockEC2)

ctx := context.Background()

if tc.mockError != nil {
mockEC2.EXPECT().LockSnapshot(ctx, &tc.input).Return(nil, tc.mockError)
} else {
mockEC2.EXPECT().LockSnapshot(ctx, &tc.input).Return(&ec2.LockSnapshotOutput{}, nil)
}

_, err := c.LockSnapshot(ctx, tc.input)

if tc.expectErr {
require.Error(t, err)
require.Equal(t, tc.mockError.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/cloud/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ type Cloud interface {
AvailabilityZones(ctx context.Context) (map[string]struct{}, error)
DryRun(ctx context.Context) error
GetInstancesPatching(ctx context.Context, nodeIDs []string) ([]*types.Instance, error)
LockSnapshot(ctx context.Context, lockOptions ec2.LockSnapshotInput) (*ec2.LockSnapshotOutput, error)
}
15 changes: 15 additions & 0 deletions pkg/cloud/mock_cloud.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions pkg/cloud/mock_ec2.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions pkg/driver/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ const (
const (
// FastSnapshotRestoreAvailabilityZones represents key for fast snapshot restore availability zones.
FastSnapshotRestoreAvailabilityZones = "fastsnapshotrestoreavailabilityzones"

// SnapshotLockMode represents a key for indicating whether snapshots are locked in Governance or Compliance mode.
SnapshotLockMode = "snapshotlockmode"

// SnapshotLockDuration is a key for the duration for which to lock the snapshots, specified in days.
SnapshotLockDuration = "snapshotlockduration"

// SnapshotLockExpirationDate is a key for specifying the expiration date for the snapshot lock, specified in the format "YYYY-MM-DDThh:mm:ss.sssZ".
SnapshotLockExpirationDate = "snapshotlockexpirationdate"

// SnapshotLockCoolOffPeriod is a key specifying the cooling-off period for compliance mode, specified in hours.
SnapshotLockCoolOffPeriod = "snapshotlockcooloffperiod"
)

// constants for volume tags and their values.
Expand Down
45 changes: 41 additions & 4 deletions pkg/driver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import (
"maps"
"strconv"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/awslabs/volume-modifier-for-k8s/pkg/rpc"
csi "github.com/container-storage-interface/spec/lib/go/csi"
"github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud"
Expand Down Expand Up @@ -857,6 +860,7 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
var vscTags []string
var fsrAvailabilityZones []string
vsProps := new(template.VolumeSnapshotProps)
vsLock := new(cloud.SnapshotLockOptions)
for key, value := range req.GetParameters() {
switch strings.ToLower(key) {
case VolumeSnapshotNameKey:
Expand All @@ -874,6 +878,26 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
} else {
return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter value %s is not a valid arn", value)
}
case SnapshotLockMode:
vsLock.LockSnapshotInput.LockMode = types.LockMode(value)
case SnapshotLockDuration:
lockDuration, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockDuration: %q", value)
}
vsLock.LockSnapshotInput.LockDuration = aws.Int32(int32(lockDuration))
case SnapshotLockExpirationDate:
expirationDate, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockExpirationDate: %q", value)
}
vsLock.LockSnapshotInput.ExpirationDate = &expirationDate
case SnapshotLockCoolOffPeriod:
lockCoolOffPeriod, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockCoolOffPeriod: %q", value)
}
vsLock.LockSnapshotInput.CoolOffPeriod = aws.Int32(int32(lockCoolOffPeriod))
default:
if strings.HasPrefix(key, TagKeyPrefix) {
vscTags = append(vscTags, value)
Expand Down Expand Up @@ -934,12 +958,18 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
if len(fsrAvailabilityZones) > 0 {
_, err := d.cloud.EnableFastSnapshotRestores(ctx, fsrAvailabilityZones, snapshot.SnapshotID)
if err != nil {
if _, deleteErr := d.cloud.DeleteSnapshot(ctx, snapshot.SnapshotID); deleteErr != nil {
return nil, status.Errorf(codes.Internal, "Could not delete snapshot ID %q: %v", snapshotName, deleteErr)
}
return nil, status.Errorf(codes.Internal, "Failed to create Fast Snapshot Restores for snapshot ID %q: %v", snapshotName, err)
return nil, d.cleanupSnapshotOnError(ctx, snapshot.SnapshotID, snapshotName, err, "Failed to create Fast Snapshot Restores")
}
}

if vsLock.LockSnapshotInput.LockMode != "" || vsLock.LockSnapshotInput.LockDuration != nil || vsLock.LockSnapshotInput.ExpirationDate != nil || vsLock.LockSnapshotInput.CoolOffPeriod != nil {
vsLock.LockSnapshotInput.SnapshotId = &snapshot.SnapshotID
_, err := d.cloud.LockSnapshot(ctx, vsLock.LockSnapshotInput)
if err != nil {
return nil, d.cleanupSnapshotOnError(ctx, snapshot.SnapshotID, snapshotName, err, "Failed to lock snapshot")
}
}

return newCreateSnapshotResponse(snapshot), nil
}

Expand Down Expand Up @@ -1297,3 +1327,10 @@ func validateFormattingOption(volumeCapabilities []*csi.VolumeCapability, paramN
func isTrue(value string) bool {
return value == trueStr
}

func (d *ControllerService) cleanupSnapshotOnError(ctx context.Context, snapshotID, snapshotName string, originalErr error, errorMsg string) error {
if _, deleteErr := d.cloud.DeleteSnapshot(ctx, snapshotID); deleteErr != nil {
return status.Errorf(codes.Internal, "Could not delete snapshot ID %q: %v", snapshotName, deleteErr)
}
return status.Errorf(codes.Internal, "%s for snapshot ID %q: %v", errorMsg, snapshotName, originalErr)
}
Loading