Skip to content

Commit 1c8f93d

Browse files
committed
Adding SnapshotLock Capabilities
1 parent 3480935 commit 1c8f93d

File tree

14 files changed

+832
-8
lines changed

14 files changed

+832
-8
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+
| snapshotLockMode | Lock mode (governance/compliance) |
7+
| snapshotLockDuration | Lock duration in days |
8+
| snapshotLockExpirationDate | Lock expiration date (RFC3339 format) |
9+
| snapshotLockCoolOffPeriod | 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 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.
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+
snapshotLockMode: "governance"
65+
snapshotLockDuration: "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+
snapshotLockMode: "compliance"
78+
snapshotLockExpirationDate: "2030-12-31T23:59:59Z"
79+
snapshotLockCoolOffPeriod: "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

examples/kubernetes/snapshot/manifests/classes/snapshotclass.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ kind: VolumeSnapshotClass
1717
metadata:
1818
name: csi-aws-vsc
1919
driver: ebs.csi.aws.com
20-
deletionPolicy: Delete
20+
deletionPolicy: Delete

hack/e2e/kops/patch-cluster.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ spec:
8686
"Effect": "Allow",
8787
"Action": [
8888
"ec2:CreateVolume",
89-
"ec2:EnableFastSnapshotRestores"
89+
"ec2:EnableFastSnapshotRestores",
90+
"ec2:LockSnapshot"
9091
],
9192
"Resource": "arn:aws:ec2:*:*:snapshot/*"
9293
},

pkg/cloud/cloud.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,17 @@ type ListSnapshotsResponse struct {
309309
NextToken string
310310
}
311311

312-
// SnapshotOptions represents parameters to create an EBS volume.
312+
// SnapshotOptions represents parameters to create an EBS snapshot.
313313
type SnapshotOptions struct {
314314
Tags map[string]string
315315
OutpostArn string
316316
}
317317

318+
// SnapshotLockOptions represents the snapshot lock specific parameters for locking en EBS snapshot.
319+
type SnapshotLockOptions struct {
320+
LockSnapshotInput ec2.LockSnapshotInput
321+
}
322+
318323
// ec2ListSnapshotsResponse is a helper struct returned from the AWS API calling function to the main ListSnapshots function.
319324
type ec2ListSnapshotsResponse struct {
320325
Snapshots []types.Snapshot
@@ -1872,6 +1877,15 @@ func (c *cloud) CreateSnapshot(ctx context.Context, volumeID string, snapshotOpt
18721877
}, nil
18731878
}
18741879

1880+
func (c *cloud) LockSnapshot(ctx context.Context, lockSnapshotInput ec2.LockSnapshotInput) (*ec2.LockSnapshotOutput, error) {
1881+
klog.InfoS("Attempting to lock Snapshot", "request parameters: ", lockSnapshotInput)
1882+
response, err := c.ec2.LockSnapshot(ctx, &lockSnapshotInput)
1883+
if err != nil {
1884+
return nil, err
1885+
}
1886+
return response, nil
1887+
}
1888+
18751889
func (c *cloud) DeleteSnapshot(ctx context.Context, snapshotID string) (success bool, err error) {
18761890
request := &ec2.DeleteSnapshotInput{}
18771891
request.SnapshotId = aws.String(snapshotID)

pkg/cloud/cloud_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5320,3 +5320,56 @@ func TestCheckIfIopsIncreaseOnExpansion(t *testing.T) {
53205320
})
53215321
}
53225322
}
5323+
5324+
func TestLockSnapshot(t *testing.T) {
5325+
testCases := []struct {
5326+
name string
5327+
input ec2.LockSnapshotInput
5328+
mockError error
5329+
expectErr bool
5330+
}{
5331+
{
5332+
name: "success: API call succeeds",
5333+
input: ec2.LockSnapshotInput{
5334+
SnapshotId: aws.String("snap-test-id"),
5335+
LockMode: types.LockModeGovernance,
5336+
LockDuration: aws.Int32(1),
5337+
},
5338+
mockError: nil,
5339+
expectErr: false,
5340+
},
5341+
{
5342+
name: "fail: AWS API error is propagated",
5343+
input: ec2.LockSnapshotInput{
5344+
SnapshotId: aws.String("snap-test-id"),
5345+
},
5346+
mockError: errors.New("InvalidSnapshot.NotFound"),
5347+
expectErr: true,
5348+
},
5349+
}
5350+
5351+
for _, tc := range testCases {
5352+
t.Run(tc.name, func(t *testing.T) {
5353+
mockCtrl := gomock.NewController(t)
5354+
mockEC2 := NewMockEC2API(mockCtrl)
5355+
c := newCloud(mockEC2)
5356+
5357+
ctx := context.Background()
5358+
5359+
if tc.mockError != nil {
5360+
mockEC2.EXPECT().LockSnapshot(ctx, &tc.input).Return(nil, tc.mockError)
5361+
} else {
5362+
mockEC2.EXPECT().LockSnapshot(ctx, &tc.input).Return(&ec2.LockSnapshotOutput{}, nil)
5363+
}
5364+
5365+
_, err := c.LockSnapshot(ctx, tc.input)
5366+
5367+
if tc.expectErr {
5368+
require.Error(t, err)
5369+
require.Equal(t, tc.mockError.Error(), err.Error())
5370+
} else {
5371+
assert.NoError(t, err)
5372+
}
5373+
})
5374+
}
5375+
}

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 ec2.LockSnapshotInput) (*ec2.LockSnapshotOutput, error)
4546
}

pkg/cloud/mock_cloud.go

Lines changed: 15 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
@@ -126,6 +126,18 @@ const (
126126
const (
127127
// FastSnapshotRestoreAvailabilityZones represents key for fast snapshot restore availability zones.
128128
FastSnapshotRestoreAvailabilityZones = "fastsnapshotrestoreavailabilityzones"
129+
130+
// SnapshotLockMode represents a key for indicating whether snapshots are locked in Governance or Compliance mode.
131+
SnapshotLockMode = "snapshotlockmode"
132+
133+
// SnapshotLockDuration is a key for the duration for which to lock the snapshots, specified in days.
134+
SnapshotLockDuration = "snapshotlockduration"
135+
136+
// SnapshotLockExpirationDate is a key for specifying the expiration date for the snapshot lock, specified in the format "YYYY-MM-DDThh:mm:ss.sssZ".
137+
SnapshotLockExpirationDate = "snapshotlockexpirationdate"
138+
139+
// SnapshotLockCoolOffPeriod is a key specifying the cooling-off period for compliance mode, specified in hours.
140+
SnapshotLockCoolOffPeriod = "snapshotlockcooloffperiod"
129141
)
130142

131143
// 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"
@@ -857,6 +860,7 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
857860
var vscTags []string
858861
var fsrAvailabilityZones []string
859862
vsProps := new(template.VolumeSnapshotProps)
863+
vsLock := new(cloud.SnapshotLockOptions)
860864
for key, value := range req.GetParameters() {
861865
switch strings.ToLower(key) {
862866
case VolumeSnapshotNameKey:
@@ -874,6 +878,26 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
874878
} else {
875879
return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter value %s is not a valid arn", value)
876880
}
881+
case SnapshotLockMode:
882+
vsLock.LockSnapshotInput.LockMode = types.LockMode(value)
883+
case SnapshotLockDuration:
884+
lockDuration, err := strconv.ParseInt(value, 10, 32)
885+
if err != nil {
886+
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockDuration: %q", value)
887+
}
888+
vsLock.LockSnapshotInput.LockDuration = aws.Int32(int32(lockDuration))
889+
case SnapshotLockExpirationDate:
890+
expirationDate, err := time.Parse(time.RFC3339, value)
891+
if err != nil {
892+
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockExpirationDate: %q", value)
893+
}
894+
vsLock.LockSnapshotInput.ExpirationDate = &expirationDate
895+
case SnapshotLockCoolOffPeriod:
896+
lockCoolOffPeriod, err := strconv.ParseInt(value, 10, 32)
897+
if err != nil {
898+
return nil, status.Errorf(codes.InvalidArgument, "Could not parse SnapshotLockCoolOffPeriod: %q", value)
899+
}
900+
vsLock.LockSnapshotInput.CoolOffPeriod = aws.Int32(int32(lockCoolOffPeriod))
877901
default:
878902
if strings.HasPrefix(key, TagKeyPrefix) {
879903
vscTags = append(vscTags, value)
@@ -934,12 +958,18 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
934958
if len(fsrAvailabilityZones) > 0 {
935959
_, err := d.cloud.EnableFastSnapshotRestores(ctx, fsrAvailabilityZones, snapshot.SnapshotID)
936960
if err != nil {
937-
if _, deleteErr := d.cloud.DeleteSnapshot(ctx, snapshot.SnapshotID); deleteErr != nil {
938-
return nil, status.Errorf(codes.Internal, "Could not delete snapshot ID %q: %v", snapshotName, deleteErr)
939-
}
940-
return nil, status.Errorf(codes.Internal, "Failed to create Fast Snapshot Restores for snapshot ID %q: %v", snapshotName, err)
961+
return nil, d.cleanupSnapshotOnError(ctx, snapshot.SnapshotID, snapshotName, err, "Failed to create Fast Snapshot Restores")
941962
}
942963
}
964+
965+
if vsLock.LockSnapshotInput.LockMode != "" || vsLock.LockSnapshotInput.LockDuration != nil || vsLock.LockSnapshotInput.ExpirationDate != nil || vsLock.LockSnapshotInput.CoolOffPeriod != nil {
966+
vsLock.LockSnapshotInput.SnapshotId = &snapshot.SnapshotID
967+
_, err := d.cloud.LockSnapshot(ctx, vsLock.LockSnapshotInput)
968+
if err != nil {
969+
return nil, d.cleanupSnapshotOnError(ctx, snapshot.SnapshotID, snapshotName, err, "Failed to lock snapshot")
970+
}
971+
}
972+
943973
return newCreateSnapshotResponse(snapshot), nil
944974
}
945975

@@ -1297,3 +1327,10 @@ func validateFormattingOption(volumeCapabilities []*csi.VolumeCapability, paramN
12971327
func isTrue(value string) bool {
12981328
return value == trueStr
12991329
}
1330+
1331+
func (d *ControllerService) cleanupSnapshotOnError(ctx context.Context, snapshotID, snapshotName string, originalErr error, errorMsg string) error {
1332+
if _, deleteErr := d.cloud.DeleteSnapshot(ctx, snapshotID); deleteErr != nil {
1333+
return status.Errorf(codes.Internal, "Could not delete snapshot ID %q: %v", snapshotName, deleteErr)
1334+
}
1335+
return status.Errorf(codes.Internal, "%s for snapshot ID %q: %v", errorMsg, snapshotName, originalErr)
1336+
}

0 commit comments

Comments
 (0)