diff --git a/pkg/connection/connection.go b/pkg/connection/connection.go index 337a81ff..b2877e62 100644 --- a/pkg/connection/connection.go +++ b/pkg/connection/connection.go @@ -47,7 +47,7 @@ type CSIConnection interface { SupportsControllerListSnapshots(ctx context.Context) (bool, error) // CreateSnapshot creates a snapshot for a volume - CreateSnapshot(ctx context.Context, snapshotName string, snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string) (driverName string, snapshotId string, timestamp int64, status *csi.SnapshotStatus, err error) + CreateSnapshot(ctx context.Context, snapshotName string, snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string, snapshotterCredentials map[string]string) (driverName string, snapshotId string, timestamp int64, status *csi.SnapshotStatus, err error) // DeleteSnapshot deletes a snapshot from a volume DeleteSnapshot(ctx context.Context, snapshotID string) (err error) @@ -189,7 +189,7 @@ func (c *csiConnection) SupportsControllerListSnapshots(ctx context.Context) (bo return false, nil } -func (c *csiConnection) CreateSnapshot(ctx context.Context, snapshotName string, snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string) (string, string, int64, *csi.SnapshotStatus, error) { +func (c *csiConnection) CreateSnapshot(ctx context.Context, snapshotName string, snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string, snapshotterCredentials map[string]string) (string, string, int64, *csi.SnapshotStatus, error) { glog.V(5).Infof("CSI CreateSnapshot: %s", snapshot.Name) if volume.Spec.CSI == nil { return "", "", 0, nil, fmt.Errorf("CSIPersistentVolumeSource not defined in spec") @@ -206,7 +206,7 @@ func (c *csiConnection) CreateSnapshot(ctx context.Context, snapshotName string, SourceVolumeId: volume.Spec.CSI.VolumeHandle, Name: snapshotName, Parameters: parameters, - CreateSnapshotSecrets: nil, + CreateSnapshotSecrets: snapshotterCredentials, } rsp, err := client.CreateSnapshot(ctx, &req) diff --git a/pkg/controller/csi_handler.go b/pkg/controller/csi_handler.go index e8bb0e8f..56836aca 100644 --- a/pkg/controller/csi_handler.go +++ b/pkg/controller/csi_handler.go @@ -30,7 +30,7 @@ import ( // Handler is responsible for handling VolumeSnapshot events from informer. type Handler interface { - CreateSnapshot(snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string) (string, string, int64, *csi.SnapshotStatus, error) + CreateSnapshot(snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string, snapshotterCredentials map[string]string) (string, string, int64, *csi.SnapshotStatus, error) DeleteSnapshot(content *crdv1.VolumeSnapshotContent) error GetSnapshotStatus(content *crdv1.VolumeSnapshotContent) (*csi.SnapshotStatus, int64, error) } @@ -57,7 +57,7 @@ func NewCSIHandler( } } -func (handler *csiHandler) CreateSnapshot(snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string) (string, string, int64, *csi.SnapshotStatus, error) { +func (handler *csiHandler) CreateSnapshot(snapshot *crdv1.VolumeSnapshot, volume *v1.PersistentVolume, parameters map[string]string, snapshotterCredentials map[string]string) (string, string, int64, *csi.SnapshotStatus, error) { ctx, cancel := context.WithTimeout(context.Background(), handler.timeout) defer cancel() @@ -66,7 +66,7 @@ func (handler *csiHandler) CreateSnapshot(snapshot *crdv1.VolumeSnapshot, volume if err != nil { return "", "", 0, nil, err } - return handler.csiConnection.CreateSnapshot(ctx, snapshotName, snapshot, volume, parameters) + return handler.csiConnection.CreateSnapshot(ctx, snapshotName, snapshot, volume, parameters, snapshotterCredentials) } func (handler *csiHandler) DeleteSnapshot(content *crdv1.VolumeSnapshotContent) error { diff --git a/pkg/controller/csi_snapshot_controller.go b/pkg/controller/csi_snapshot_controller.go index 9bfd9829..d2806c71 100644 --- a/pkg/controller/csi_snapshot_controller.go +++ b/pkg/controller/csi_snapshot_controller.go @@ -77,6 +77,9 @@ const pvcKind = "PersistentVolumeClaim" const IsDefaultSnapshotClassAnnotation = "snapshot.storage.kubernetes.io/is-default-class" +const snapshotterSecretNameKey = "csiSnapshotterSecretName" +const snapshotterSecretNamespaceKey = "csiSnapshotterSecretNamespace" + // syncContent deals with one key off the queue. It returns false when it's time to quit. func (ctrl *CSISnapshotController) syncContent(content *crdv1.VolumeSnapshotContent) error { glog.V(4).Infof("synchronizing VolumeSnapshotContent[%s]", content.Name) @@ -476,7 +479,20 @@ func (ctrl *CSISnapshotController) createSnapshotOperation(snapshot *crdv1.Volum return nil, err } - driverName, snapshotID, timestamp, csiSnapshotStatus, err := ctrl.handler.CreateSnapshot(snapshot, volume, class.Parameters) + // Create VolumeSnapshotContent name + contentName := GetSnapshotContentNameForSnapshot(snapshot) + + // Resolve snapshotting secret credentials. + snapshotterSecretRef, err := GetSecretReference(snapshotterSecretNameKey, snapshotterSecretNamespaceKey, class.Parameters, contentName, nil) + if err != nil { + return nil, err + } + snapshotterCredentials, err := GetCredentials(ctrl.client, snapshotterSecretRef) + if err != nil { + return nil, err + } + + driverName, snapshotID, timestamp, csiSnapshotStatus, err := ctrl.handler.CreateSnapshot(snapshot, volume, class.Parameters, snapshotterCredentials) if err != nil { return nil, fmt.Errorf("Failed to take snapshot of the volume, %s: %q", volume.Name, err) } @@ -489,7 +505,6 @@ func (ctrl *CSISnapshotController) createSnapshotOperation(snapshot *crdv1.Volum } // Create VolumeSnapshotContent in the database - contentName := GetSnapshotContentNameForSnapshot(snapshot) volumeRef, err := ref.GetReference(scheme.Scheme, volume) snapshotContent := &crdv1.VolumeSnapshotContent{ diff --git a/pkg/controller/util.go b/pkg/controller/util.go index c9d10285..9ae3bc41 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -23,7 +23,11 @@ import ( "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "os" "strconv" "strings" ) @@ -121,3 +125,118 @@ func IsDefaultAnnotation(obj metav1.ObjectMeta) bool { return false } + +// GetSecretReference returns a reference to the secret specified in the given nameKey and namespaceKey parameters, or an error if the parameters are not specified correctly. +// if neither the name or namespace parameter are set, a nil reference and no error is returned. +// no lookup of the referenced secret is performed, and the secret may or may not exist. +// +// supported tokens for name resolution: +// - ${volumesnapshotcontent.name} +// - ${volumesnapshot.namespace} +// - ${volumesnapshot.name} +// - ${volumesnapshot.annotations['ANNOTATION_KEY']} (e.g. ${pvc.annotations['example.com/snapshot-create-secret-name']}) +// +// supported tokens for namespace resolution: +// - ${volumesnapshotcontent.name} +// - ${volumesnapshot.namespace} +// +// an error is returned in the following situations: +// - only one of name or namespace is provided +// - the name or namespace parameter contains a token that cannot be resolved +// - the resolved name is not a valid secret name +// - the resolved namespace is not a valid namespace name +func GetSecretReference(nameKey, namespaceKey string, snapshotClassParams map[string]string, snapContentName string, snapshot *crdv1.VolumeSnapshot) (*v1.SecretReference, error) { + nameTemplate, hasName := snapshotClassParams[nameKey] + namespaceTemplate, hasNamespace := snapshotClassParams[namespaceKey] + + if !hasName && !hasNamespace { + return nil, nil + } + + if len(nameTemplate) == 0 || len(namespaceTemplate) == 0 { + return nil, fmt.Errorf("%s and %s parameters must be specified together", nameKey, namespaceKey) + } + + ref := &v1.SecretReference{} + + { + // Secret namespace template can make use of the VolumeSnapshotContent name or the VolumeSnapshot namespace. + // Note that neither of those things are under the control of the VolumeSnapshot user. + namespaceParams := map[string]string{"volumesnapshotcontent.name": snapContentName} + if snapshot != nil { + namespaceParams["volumesnapshot.namespace"] = snapshot.Namespace + } + + resolvedNamespace, err := resolveTemplate(namespaceTemplate, namespaceParams) + if err != nil { + return nil, fmt.Errorf("error resolving %s value %q: %v", namespaceKey, namespaceTemplate, err) + } + if len(validation.IsDNS1123Label(resolvedNamespace)) > 0 { + if namespaceTemplate != resolvedNamespace { + return nil, fmt.Errorf("%s parameter %q resolved to %q which is not a valid namespace name", namespaceKey, namespaceTemplate, resolvedNamespace) + } + return nil, fmt.Errorf("%s parameter %q is not a valid namespace name", namespaceKey, namespaceTemplate) + } + ref.Namespace = resolvedNamespace + } + + { + // Secret name template can make use of the VolumeSnapshotContent name, VolumeSnapshot name or namespace, + // or a VolumeSnapshot annotation. + // Note that VolumeSnapshot name and annotations are under the VolumeSnapshot user's control. + nameParams := map[string]string{"volumesnapshotcontent.name": snapContentName} + if snapshot != nil { + nameParams["volumesnapshot.name"] = snapshot.Name + nameParams["volumesnapshot.namespace"] = snapshot.Namespace + for k, v := range snapshot.Annotations { + nameParams["volumesnapshot.annotations['"+k+"']"] = v + } + } + resolvedName, err := resolveTemplate(nameTemplate, nameParams) + if err != nil { + return nil, fmt.Errorf("error resolving %s value %q: %v", nameKey, nameTemplate, err) + } + if len(validation.IsDNS1123Subdomain(resolvedName)) > 0 { + if nameTemplate != resolvedName { + return nil, fmt.Errorf("%s parameter %q resolved to %q which is not a valid secret name", nameKey, nameTemplate, resolvedName) + } + return nil, fmt.Errorf("%s parameter %q is not a valid secret name", nameKey, nameTemplate) + } + ref.Name = resolvedName + } + + glog.V(4).Infof("GetSecretReference validated Secret: %+v", ref) + return ref, nil +} + +func resolveTemplate(template string, params map[string]string) (string, error) { + missingParams := sets.NewString() + resolved := os.Expand(template, func(k string) string { + v, ok := params[k] + if !ok { + missingParams.Insert(k) + } + return v + }) + if missingParams.Len() > 0 { + return "", fmt.Errorf("invalid tokens: %q", missingParams.List()) + } + return resolved, nil +} + +func GetCredentials(k8s kubernetes.Interface, ref *v1.SecretReference) (map[string]string, error) { + if ref == nil { + return nil, nil + } + + secret, err := k8s.CoreV1().Secrets(ref.Namespace).Get(ref.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error getting secret %s in namespace %s: %v", ref.Name, ref.Namespace, err) + } + + credentials := map[string]string{} + for key, value := range secret.Data { + credentials[key] = string(value) + } + return credentials, nil +}