Skip to content

Commit 1f20329

Browse files
committed
Adding SnapshotLock Capabilities
1 parent a419186 commit 1f20329

File tree

15 files changed

+858
-7
lines changed

15 files changed

+858
-7
lines changed

docs/snapshot.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
| Parameter | Description of value |
33
|------------------------------------|-----------------------------------------------------------|
44
| fastSnapshotRestoreAvailabilityZones | Comma separated list of availability zones |
5-
| outpostArn | Arn of the outpost you wish to have the snapshot saved to |
5+
| outpostArn | Arn of the outpost you wish to have the snapshot saved to |
6+
| lockMode | Lock mode (governance/compliance) |
7+
| lockDuration | Lock duration in days |
8+
| lockExpirationDate | Lock expiration date (RFC3339 format) |
9+
| lockCoolOffPeriod | Cool-off period in hours (compliance mode only) |
610

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

4549
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.
4650

51+
# Snapshot Lock
52+
53+
The EBS CSI Driver supports [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.
54+
55+
**Example - Lock in Governance Mode with Specified Duration**
56+
```yaml
57+
apiVersion: snapshot.storage.k8s.io/v1
58+
kind: VolumeSnapshotClass
59+
metadata:
60+
name: csi-aws-vsc-locked
61+
driver: ebs.csi.aws.com
62+
deletionPolicy: Delete
63+
parameters:
64+
lockMode: "governance"
65+
lockDuration: "7"
66+
```
67+
68+
**Example - Lock in Compliance Mode with Expiration Date and Cool Off Period**
69+
```yaml
70+
apiVersion: snapshot.storage.k8s.io/v1
71+
kind: VolumeSnapshotClass
72+
metadata:
73+
name: csi-aws-vsc-compliance
74+
driver: ebs.csi.aws.com
75+
deletionPolicy: Delete
76+
parameters:
77+
lockMode: "compliance"
78+
lockExpirationDate: "2030-12-31T23:59:59Z"
79+
lockCoolOffPeriod: "24"
80+
```
81+
82+
## Failure Mode
83+
84+
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.
85+
4786

4887
# Amazon EBS Local Snapshots on Outposts
4988

hack/e2e/kops/patch-cluster.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ spec:
7272
"Effect": "Allow",
7373
"Action": [
7474
"ec2:CreateVolume",
75-
"ec2:EnableFastSnapshotRestores"
75+
"ec2:EnableFastSnapshotRestores",
76+
"ec2:LockSnapshot"
7677
],
7778
"Resource": "arn:aws:ec2:*:*:snapshot/*"
7879
},

pkg/cloud/cloud.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,21 @@ type ListSnapshotsResponse struct {
313313
NextToken string
314314
}
315315

316-
// SnapshotOptions represents parameters to create an EBS volume.
316+
// SnapshotOptions represents parameters to create an EBS snapshot.
317317
type SnapshotOptions struct {
318318
Tags map[string]string
319319
OutpostArn string
320320
}
321321

322+
// SnapshotLockOptions represents parameters to lock an EBS snapshot.
323+
type SnapshotLockOptions struct {
324+
SnapshotId *string
325+
LockMode types.LockMode
326+
CoolOffPeriod *int32
327+
ExpirationDate *time.Time
328+
LockDuration *int32
329+
}
330+
322331
// ec2ListSnapshotsResponse is a helper struct returned from the AWS API calling function to the main ListSnapshots function.
323332
type ec2ListSnapshotsResponse struct {
324333
Snapshots []types.Snapshot
@@ -1961,6 +1970,21 @@ func (c *cloud) CreateSnapshot(ctx context.Context, volumeID string, snapshotOpt
19611970
}, nil
19621971
}
19631972

1973+
func (c *cloud) LockSnapshot(ctx context.Context, lockOptions *SnapshotLockOptions) error {
1974+
lockSnapshotInput := ec2.LockSnapshotInput{
1975+
SnapshotId: lockOptions.SnapshotId,
1976+
LockMode: lockOptions.LockMode,
1977+
CoolOffPeriod: lockOptions.CoolOffPeriod,
1978+
ExpirationDate: lockOptions.ExpirationDate,
1979+
LockDuration: lockOptions.LockDuration,
1980+
}
1981+
_, err := c.ec2.LockSnapshot(ctx, &lockSnapshotInput)
1982+
if err != nil {
1983+
return err
1984+
}
1985+
return nil
1986+
}
1987+
19641988
func (c *cloud) DeleteSnapshot(ctx context.Context, snapshotID string) (success bool, err error) {
19651989
request := &ec2.DeleteSnapshotInput{}
19661990
request.SnapshotId = aws.String(snapshotID)

pkg/cloud/cloud_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5509,3 +5509,64 @@ func TestExecBatchCaching(t *testing.T) {
55095509
assert.False(t, exists, "snap-0c7e1a5f8b2d4c939 should be removed from cache after successful retry")
55105510
})
55115511
}
5512+
5513+
func TestLockSnapshot(t *testing.T) {
5514+
testCases := []struct {
5515+
name string
5516+
input *SnapshotLockOptions
5517+
mockError error
5518+
expectErr bool
5519+
}{
5520+
{
5521+
name: "success: API call succeeds",
5522+
input: &SnapshotLockOptions{
5523+
SnapshotId: aws.String("snap-test-id"),
5524+
LockMode: types.LockModeGovernance,
5525+
LockDuration: aws.Int32(1),
5526+
},
5527+
mockError: nil,
5528+
expectErr: false,
5529+
},
5530+
{
5531+
name: "fail: AWS API error is propagated",
5532+
input: &SnapshotLockOptions{
5533+
SnapshotId: aws.String("snap-test-id"),
5534+
},
5535+
mockError: errors.New("InvalidSnapshot.NotFound"),
5536+
expectErr: true,
5537+
},
5538+
}
5539+
5540+
for _, tc := range testCases {
5541+
t.Run(tc.name, func(t *testing.T) {
5542+
mockCtrl := gomock.NewController(t)
5543+
mockEC2 := NewMockEC2API(mockCtrl)
5544+
c := newCloud(mockEC2)
5545+
5546+
ctx := context.Background()
5547+
5548+
expectedInput := &ec2.LockSnapshotInput{
5549+
SnapshotId: tc.input.SnapshotId,
5550+
LockMode: tc.input.LockMode,
5551+
CoolOffPeriod: tc.input.CoolOffPeriod,
5552+
ExpirationDate: tc.input.ExpirationDate,
5553+
LockDuration: tc.input.LockDuration,
5554+
}
5555+
5556+
if tc.mockError != nil {
5557+
mockEC2.EXPECT().LockSnapshot(ctx, expectedInput).Return(nil, tc.mockError)
5558+
} else {
5559+
mockEC2.EXPECT().LockSnapshot(ctx, expectedInput).Return(&ec2.LockSnapshotOutput{}, nil)
5560+
}
5561+
5562+
err := c.LockSnapshot(ctx, tc.input)
5563+
5564+
if tc.expectErr {
5565+
require.Error(t, err)
5566+
require.Equal(t, tc.mockError.Error(), err.Error())
5567+
} else {
5568+
assert.NoError(t, err)
5569+
}
5570+
})
5571+
}
5572+
}

pkg/cloud/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ type Cloud interface {
4242
AvailabilityZones(ctx context.Context) (map[string]struct{}, error)
4343
DryRun(ctx context.Context) error
4444
GetInstancesPatching(ctx context.Context, nodeIDs []string) ([]*types.Instance, error)
45+
LockSnapshot(ctx context.Context, lockOptions *SnapshotLockOptions) (err error)
4546
}

pkg/cloud/mock_cloud.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cloud/mock_ec2.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/driver/constants.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ const (
129129
const (
130130
// FastSnapshotRestoreAvailabilityZones represents key for fast snapshot restore availability zones.
131131
FastSnapshotRestoreAvailabilityZones = "fastsnapshotrestoreavailabilityzones"
132+
133+
// LockMode represents a key for indicating whether snapshots are locked in Governance or Compliance mode.
134+
LockMode = "lockmode"
135+
136+
// LockDuration is a key for the duration for which to lock the snapshots, specified in days.
137+
LockDuration = "lockduration"
138+
139+
// LockExpirationDate is a key for specifying the expiration date for the snapshot lock, specified in the format "YYYY-MM-DDThh:mm:ss.sssZ".
140+
LockExpirationDate = "lockexpirationdate"
141+
142+
// LockCoolOffPeriod is a key specifying the cooling-off period for compliance mode, specified in hours.
143+
LockCoolOffPeriod = "lockcooloffperiod"
132144
)
133145

134146
// constants for volume tags and their values.

pkg/driver/controller.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ import (
2323
"maps"
2424
"strconv"
2525
"strings"
26+
"time"
2627

28+
"github.com/aws/aws-sdk-go-v2/aws"
2729
"github.com/aws/aws-sdk-go-v2/aws/arn"
30+
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
2831
"github.com/awslabs/volume-modifier-for-k8s/pkg/rpc"
2932
csi "github.com/container-storage-interface/spec/lib/go/csi"
3033
"github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud"
@@ -866,6 +869,7 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
866869
var vscTags []string
867870
var fsrAvailabilityZones []string
868871
vsProps := new(template.VolumeSnapshotProps)
872+
vsLock := new(cloud.SnapshotLockOptions)
869873
for key, value := range req.GetParameters() {
870874
switch strings.ToLower(key) {
871875
case VolumeSnapshotNameKey:
@@ -883,6 +887,26 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
883887
} else {
884888
return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter value %s is not a valid arn", value)
885889
}
890+
case LockMode:
891+
vsLock.LockMode = types.LockMode(value)
892+
case LockDuration:
893+
lockDuration, err := strconv.ParseInt(value, 10, 32)
894+
if err != nil {
895+
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockDuration: %q", value)
896+
}
897+
vsLock.LockDuration = aws.Int32(int32(lockDuration))
898+
case LockExpirationDate:
899+
expirationDate, err := time.Parse(time.RFC3339, value)
900+
if err != nil {
901+
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockExpirationDate: %q", value)
902+
}
903+
vsLock.ExpirationDate = &expirationDate
904+
case LockCoolOffPeriod:
905+
lockCoolOffPeriod, err := strconv.ParseInt(value, 10, 32)
906+
if err != nil {
907+
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockCoolOffPeriod: %q", value)
908+
}
909+
vsLock.CoolOffPeriod = aws.Int32(int32(lockCoolOffPeriod))
886910
default:
887911
if strings.HasPrefix(key, TagKeyPrefix) {
888912
vscTags = append(vscTags, value)
@@ -943,12 +967,18 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
943967
if len(fsrAvailabilityZones) > 0 {
944968
_, err := d.cloud.EnableFastSnapshotRestores(ctx, fsrAvailabilityZones, snapshot.SnapshotID)
945969
if err != nil {
946-
if _, deleteErr := d.cloud.DeleteSnapshot(ctx, snapshot.SnapshotID); deleteErr != nil {
947-
return nil, status.Errorf(codes.Internal, "Could not delete snapshot ID %q: %v", snapshotName, deleteErr)
948-
}
949-
return nil, status.Errorf(codes.Internal, "Failed to create Fast Snapshot Restores for snapshot ID %q: %v", snapshotName, err)
970+
return nil, d.cleanupSnapshotOnError(ctx, snapshot.SnapshotID, snapshotName, err, "Failed to create Fast Snapshot Restores")
950971
}
951972
}
973+
974+
if vsLock.LockMode != "" || vsLock.LockDuration != nil || vsLock.ExpirationDate != nil || vsLock.CoolOffPeriod != nil {
975+
vsLock.SnapshotId = &snapshot.SnapshotID
976+
err := d.cloud.LockSnapshot(ctx, vsLock)
977+
if err != nil {
978+
return nil, d.cleanupSnapshotOnError(ctx, snapshot.SnapshotID, snapshotName, err, "Failed to lock snapshot")
979+
}
980+
}
981+
952982
return newCreateSnapshotResponse(snapshot), nil
953983
}
954984

@@ -1306,3 +1336,10 @@ func validateFormattingOption(volumeCapabilities []*csi.VolumeCapability, paramN
13061336
func isTrue(value string) bool {
13071337
return value == trueStr
13081338
}
1339+
1340+
func (d *ControllerService) cleanupSnapshotOnError(ctx context.Context, snapshotID, snapshotName string, originalErr error, errorMsg string) error {
1341+
if _, deleteErr := d.cloud.DeleteSnapshot(ctx, snapshotID); deleteErr != nil {
1342+
return status.Errorf(codes.Internal, "Could not delete snapshot ID %q: %v", snapshotName, deleteErr)
1343+
}
1344+
return status.Errorf(codes.Internal, "%s for snapshot ID %q: %v", errorMsg, snapshotName, originalErr)
1345+
}

0 commit comments

Comments
 (0)