Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support interpolated snapshot tags #1558

Merged
Merged
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
3 changes: 3 additions & 0 deletions charts/aws-ebs-csi-driver/templates/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ spec:
args:
- --csi-address=$(ADDRESS)
- --leader-election=true
{{- if .Values.controller.extraCreateMetadata }}
- --extra-create-metadata
torredil marked this conversation as resolved.
Show resolved Hide resolved
{{- end}}
hanyuel marked this conversation as resolved.
Show resolved Hide resolved
env:
- name: ADDRESS
value: /var/lib/csi/sockets/pluginproxy/csi.sock
Expand Down
1 change: 1 addition & 0 deletions deploy/kubernetes/base/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ spec:
args:
- --csi-address=$(ADDRESS)
- --leader-election=true
- --extra-create-metadata
torredil marked this conversation as resolved.
Show resolved Hide resolved
env:
- name: ADDRESS
value: /var/lib/csi/sockets/pluginproxy/csi.sock
Expand Down
34 changes: 34 additions & 0 deletions docs/tagging.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,40 @@ backup=true
billingID=ABCDEF
```

# Snapshot Tagging
hanyuel marked this conversation as resolved.
Show resolved Hide resolved
The AWS EBS CSI Driver supports tagging snapshots through `VolumeSnapshotClass.parameters`, similarly to StorageClass tagging.

The CSI driver supports runtime string interpolation on the snapshot tag values. You can specify placeholders for VolumeSnapshot namespace, VolumeSnapshot name and VolumeSnapshotContent name, which will then be dynamically computed at runtime. You can also use the functions provided by the CSI Driver to apply more expressive tags. **Note: Interpolated tags require the `--extra-create-metadata` flag to be enabled on the `external-snapshotter` sidecar.**

**Example**
```
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-aws-vsc
driver: ebs.csi.aws.com
deletionPolicy: Delete
parameters:
tagSpecification_1: "key1=value1"
tagSpecification_2: "key2="
# Interpolated tag
tagSpecification_3: "snapshotnamespace={{ .VolumeSnapshotNamespace }}"
tagSpecification_4: "snapshotname={{ .VolumeSnapshotName }}"
tagSpecification_5: "snapshotcontentname={{ .VolumeSnapshotContentName }}"
# Interpolated tag w/ function
tagSpecification_6: 'key6={{ .VolumeSnapshotNamespace | contains "prod" }}'
```

Provisioning a snapshot in namespace 'ns-prod' with `VolumeSnapshot` name being 'ebs-snapshot' using this VolumeSnapshotClass, will apply the following tags to the snapshot:

```
key1=value1
key2=<empty string>
snapshotnamespace=ns-prod
snapshotname=ebs-snapshot
snapshotcontentname=<the computed VolumeSnapshotContent name>
key6=true
```
____

## Failure Modes
Expand Down
10 changes: 10 additions & 0 deletions pkg/driver/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ const (
// provisioned volume
PVNameKey = "csi.storage.k8s.io/pv/name"

// VolumeSnapshotNameKey contains name of the snapshot
VolumeSnapshotNameKey = "csi.storage.k8s.io/volumesnapshot/name"

// VolumeSnapshotNamespaceKey contains namespace of the snapshot
VolumeSnapshotNamespaceKey = "csi.storage.k8s.io/volumesnapshot/namespace"

// VolumeSnapshotCotentNameKey contains name of the VolumeSnapshotContent that is the source
// for the snapshot
VolumeSnapshotContentNameKey = "csi.storage.k8s.io/volumesnapshotcontent/name"

// BlockExpressKey increases the iops limit for io2 volumes to the block express limit
BlockExpressKey = "blockexpress"

Expand Down
22 changes: 16 additions & 6 deletions pkg/driver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
blockSize string
)

tProps := new(template.Props)
tProps := new(template.PVProps)

for key, value := range req.GetParameters() {
switch strings.ToLower(key) {
Expand Down Expand Up @@ -605,15 +605,25 @@ func (d *controllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
}

var vscTags []string
vsProps := new(template.VolumeSnapshotProps)
for key, value := range req.GetParameters() {
if strings.HasPrefix(key, TagKeyPrefix) {
vscTags = append(vscTags, value)
} else {
return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter key %s for CreateSnapshot", key)
switch key {
case VolumeSnapshotNameKey:
vsProps.VolumeSnapshotName = value
case VolumeSnapshotNamespaceKey:
vsProps.VolumeSnapshotNamespace = value
case VolumeSnapshotContentNameKey:
vsProps.VolumeSnapshotContentName = value
default:
if strings.HasPrefix(key, TagKeyPrefix) {
vscTags = append(vscTags, value)
} else {
return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter key %s for CreateSnapshot", key)
}
}
}

addTags, err := template.Evaluate(vscTags, nil, d.driverOptions.warnOnInvalidTag)
addTags, err := template.Evaluate(vscTags, vsProps, d.driverOptions.warnOnInvalidTag)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Error interpolating the tag value: %v", err)
}
Expand Down
12 changes: 9 additions & 3 deletions pkg/util/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import (
"k8s.io/klog/v2"
)

type Props struct {
type PVProps struct {
PVCName string
PVCNamespace string
PVName string
}

func Evaluate(tm []string, props *Props, warnOnly bool) (map[string]string, error) {
type VolumeSnapshotProps struct {
VolumeSnapshotName string
VolumeSnapshotNamespace string
VolumeSnapshotContentName string
}

func Evaluate(tm []string, props interface{}, warnOnly bool) (map[string]string, error) {
md := make(map[string]string)
for _, s := range tm {
st := strings.SplitN(s, "=", 2)
Expand All @@ -39,7 +45,7 @@ func Evaluate(tm []string, props *Props, warnOnly bool) (map[string]string, erro
return md, nil
}

func execTemplate(value string, props *Props, t *template.Template) (string, error) {
func execTemplate(value string, props interface{}, t *template.Template) (string, error) {
tmpl, err := t.Parse(value)
if err != nil {
return "", err
Expand Down
143 changes: 142 additions & 1 deletion pkg/util/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func TestEvaluate(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

props := &Props{
props := &PVProps{
PVCName: tc.pvcName,
PVCNamespace: tc.pvcNamespace,
PVName: tc.pvName,
Expand All @@ -212,3 +212,144 @@ func TestEvaluate(t *testing.T) {
})
}
}

func TestEvaluateVolumeSnapshotTemplate(t *testing.T) {
testCases := []struct {
name string
input []string
volumeSnapshotName string
volumeSnapshotNamespace string
volumeSnapshotContentName string
warnOnly bool
expectErr bool
expectedTags map[string]string
}{
{
name: "simple substitution",
input: []string{
"key1={{ .VolumeSnapshotName }}",
"key2={{ .VolumeSnapshotNamespace }}",
"key3={{ .VolumeSnapshotContentName }}",
},
volumeSnapshotName: "ebs-vs",
volumeSnapshotNamespace: "default",
volumeSnapshotContentName: "ebs-vs-content-012345",
expectedTags: map[string]string{
"key1": "ebs-vs",
"key2": "default",
"key3": "ebs-vs-content-012345",
},
},
{
name: "template parsing error",
input: []string{
"key1={{ .VolumeSnapshotName }",
},
expectErr: true,
},
{
name: "template parsing error warn only",
input: []string{
"key1={{ .VolumeSnapshotName }",
"key2={{ .VolumeSnapshotNamespace }}",
},
volumeSnapshotName: "ebs-vs",
volumeSnapshotNamespace: "default",
warnOnly: true,
expectedTags: map[string]string{
"key2": "default",
},
},
{
name: "test unsupported func - returns error",
input: []string{
`backup={{ .VolumeSnapshotNamespace | html }}`,
},
volumeSnapshotNamespace: "ns-prod",
expectErr: true,
},
{
name: "test function - contains",
input: []string{
`backup={{ .VolumeSnapshotNamespace | contains "prod" }}`,
},
volumeSnapshotNamespace: "ns-prod",
expectedTags: map[string]string{
"backup": "true",
},
},
{
name: "test function - toUpper",
input: []string{
`backup={{ .VolumeSnapshotNamespace | toUpper }}`,
},
volumeSnapshotNamespace: "ns-prod",
expectedTags: map[string]string{
"backup": "NS-PROD",
},
},
{
name: "test function - toLower",
input: []string{
`backup={{ .VolumeSnapshotNamespace | toLower }}`,
},
volumeSnapshotNamespace: "ns-PROD",
expectedTags: map[string]string{
"backup": "ns-prod",
},
},
{
name: "test function - field",
input: []string{
`backup={{ .VolumeSnapshotNamespace | field "-" 1 }}`,
},
volumeSnapshotNamespace: "ns-prod-default",
expectedTags: map[string]string{
"backup": "prod",
},
},
{
name: "test function - substring",
input: []string{
`key1={{ .VolumeSnapshotNamespace | substring 0 4 }}`,
},
volumeSnapshotNamespace: "prod-12345",
expectedTags: map[string]string{
"key1": "prod",
},
},
{
name: "field returns error",
input: []string{
`key1={{ .VolumeSnapshotNamespace | field "-" 1 }}`,
},
expectErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

props := &VolumeSnapshotProps{
VolumeSnapshotName: tc.volumeSnapshotName,
VolumeSnapshotNamespace: tc.volumeSnapshotNamespace,
VolumeSnapshotContentName: tc.volumeSnapshotContentName,
}

tags, err := Evaluate(tc.input, props, tc.warnOnly)

if tc.expectErr {
if err == nil {
t.Fatalf("expected an error; got nil")
}
} else {
if err != nil {
t.Fatalf("err is not nil; err = %v", err)
}
if diff := cmp.Diff(tc.expectedTags, tags); diff != "" {
t.Fatalf("tags are different; diff = %v", diff)
}
}
})
}
}