From 32ee704e9f9c747a525f526b43af9e8e8e50314b Mon Sep 17 00:00:00 2001 From: Ardalan Kangarlou Date: Tue, 14 Nov 2017 13:40:25 -0500 Subject: [PATCH] Added support to clone a volume by referring to the corresponding PVC name Not quite a solution to #13 but a first step to support clones with Trident. --- CHANGELOG.md | 1 + core/orchestrator_core.go | 9 +-- core/orchestrator_core_test.go | 6 +- docs/reference/concepts/objects.rst | 50 ++++++++++--- frontend/docker/plugin.go | 42 +++++------ frontend/docker/volumes.go | 36 +++++----- frontend/kubernetes/config.go | 2 + frontend/kubernetes/plugin.go | 71 +++++++++++++++---- frontend/kubernetes/volumes.go | 27 ++++--- storage/backend.go | 7 +- storage/eseries/eseries.go | 3 +- storage/fake/fake.go | 5 +- storage/ontap/ontap_common.go | 5 +- storage/ontap/ontap_nas.go | 1 + storage/ontap/ontap_nas_qtree.go | 1 + storage/ontap/ontap_san.go | 1 + storage/solidfire/solidfire_san.go | 6 +- storage/volume.go | 42 +++++------ storage_attribute/common_attributes.go | 2 + storage_attribute/request.go | 4 +- storage_class/test_utils/utils.go | 4 +- ...{pvc-basic-v1.yaml => pvc-basic-beta.yaml} | 3 +- .../sample-input/pvc-basic-clone.yaml | 13 ++++ trident-installer/sample-input/pvc-basic.yaml | 3 +- ...=> storage-class-basic-v1beta1.yaml.templ} | 2 +- .../storage-class-basic.yaml.templ | 2 +- .../storage-class-ontap-gold.yaml | 2 +- .../storage-class-solidfire-bronze.yaml | 2 +- 28 files changed, 224 insertions(+), 128 deletions(-) rename trident-installer/sample-input/{pvc-basic-v1.yaml => pvc-basic-beta.yaml} (70%) create mode 100644 trident-installer/sample-input/pvc-basic-clone.yaml rename trident-installer/sample-input/{storage-class-basic-v1.yaml.templ => storage-class-basic-v1beta1.yaml.templ} (77%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a052772c..ffbf7602a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Enabled Trident installation on an EF-series (all-flash) array. **Enhancements:** +- Enabled cloning volumes via a new PVC annotation. - Added CHAP support for SolidFire (Issue [#42](https://github.com/NetApp/trident/issues/42)). - Added support for the etcdv3 API (Issue [#45](https://github.com/NetApp/trident/issues/45)). - Added the etcd-copy utility to migrate data between etcd clusters. diff --git a/core/orchestrator_core.go b/core/orchestrator_core.go index 8c43c1797..bb0cfd08d 100644 --- a/core/orchestrator_core.go +++ b/core/orchestrator_core.go @@ -648,9 +648,10 @@ func (o *tridentOrchestrator) CloneVolume( volumeConfig.Version = config.OrchestratorAPIVersion // Get the source volume - sourceVolume, found := o.volumes[volumeConfig.SourceName] + sourceVolume, found := o.volumes[volumeConfig.CloneSourceVolume] if !found { - return nil, fmt.Errorf("Source volume not found: %s", volumeConfig.SourceName) + return nil, fmt.Errorf("Source volume not found: %s", + volumeConfig.CloneSourceVolume) } sourceVolumeConfig := sourceVolume.Config @@ -661,8 +662,8 @@ func (o *tridentOrchestrator) CloneVolume( // Copy a few attributes from the request that will affect clone creation cloneConfig.Name = volumeConfig.Name cloneConfig.SplitOnClone = volumeConfig.SplitOnClone - cloneConfig.SourceName = volumeConfig.SourceName - cloneConfig.SourceSnapshotName = volumeConfig.SourceSnapshotName + cloneConfig.CloneSourceVolume = volumeConfig.CloneSourceVolume + cloneConfig.CloneSourceSnapshot = volumeConfig.CloneSourceSnapshot // Add transaction in case the operation must be rolled back later volTxn, err := o.addVolumeTransaction(volumeConfig) diff --git a/core/orchestrator_core_test.go b/core/orchestrator_core_test.go index 2859ef870..80dc74113 100644 --- a/core/orchestrator_core_test.go +++ b/core/orchestrator_core_test.go @@ -1006,9 +1006,9 @@ func TestCloneVolumes(t *testing.T) { // Now clone the volume and ensure everything looks fine cloneName := s.config.Name + "_clone" cloneConfig := &storage.VolumeConfig{ - Name: cloneName, - StorageClass: s.config.StorageClass, - SourceName: s.config.Name, + Name: cloneName, + StorageClass: s.config.StorageClass, + CloneSourceVolume: s.config.Name, } cloneResult, err := orchestrator.CloneVolume(cloneConfig) if err != nil { diff --git a/docs/reference/concepts/objects.rst b/docs/reference/concepts/objects.rst index 93e666e2b..211ad874b 100644 --- a/docs/reference/concepts/objects.rst +++ b/docs/reference/concepts/objects.rst @@ -66,14 +66,16 @@ defaults that you set in the backend configuration: =================================== ================= ====================================================== Annotation Volume Option Supported Drivers =================================== ================= ====================================================== +trident.netapp.io/fileSystem fileSystem ontap-san, solidfire-san, eseries-iscsi +trident.netapp.io/reclaimPolicy N/A any +trident.netapp.io/cloneFromPVC cloneSourceVolume ontap-nas, ontap-san, solidfire-san +trident.netapp.io/splitOnClone splitOnClone ontap-nas, ontap-san trident.netapp.io/protocol protocol any trident.netapp.io/exportPolicy exportPolicy ontap-nas, ontap-nas-economy trident.netapp.io/snapshotPolicy snapshotPolicy ontap-nas, ontap-nas-economy, ontap-san trident.netapp.io/snapshotDirectory snapshotDirectory ontap-nas, ontap-nas-economy trident.netapp.io/unixPermissions unixPermissions ontap-nas, ontap-nas-economy trident.netapp.io/blockSize blockSize solidfire-san -trident.netapp.io/fileSystem fileSystem ontap-san, solidfire-san, eseries-iscsi -trident.netapp.io/reclaimPolicy N/A any =================================== ================= ====================================================== The reclaim policy for the created PV can be determined by setting the @@ -88,12 +90,35 @@ manually deleted. If the PV uses the ``Retain`` policy, Trident ignores it and assumes the administrator will clean it up from Kubernetes and the backend, allowing the volume to be backed up or inspected before its removal. Note that deleting the PV will not cause Trident to delete the backing volume; it must be -removed manually via the REST API. - -``sample-input/pvc-basic.yaml`` and ``sample-input/pvc-full.yaml`` contain -examples of PVC definitions for use with Trident. See -:ref:`Trident Volume objects` for a full description of the parameters and -settings associated with Trident volumes. +removed manually via the REST API (i.e., ``tridentctl``). + +One novel aspect of Trident is that users can provision new volumes by cloning +existing volumes. Trident enables this functionality via the PVC annotation +``trident.netapp.io/cloneFromPVC``. For example, if a user already has a PVC +called ``mysql``, she can create a new PVC called ``mysqlclone`` by referring +to the ``mysql`` PVC: ``trident.netapp.io/cloneFromPVC: mysql``. With this +annotation set, Trident clones the volume corresponding to the ``mysql`` PVC, +instead of provisioning a volume from scratch. A few points worth considering +are the following: (1) We recommend cloning an idle volume, (2) a PVC and its +clone must be in the same Kubernetes namespace and have the same storage class, +and (3) with ``ontap-\*`` drivers, it might be desirable to set the PVC +annotation ``trident.netapp.io/splitOnClone`` in conjunction with +``trident.netapp.io/cloneFromPVC``. With ``trident.netapp.io/splitOnClone`` set +to ``true``, Trident splits the cloned volume from the parent volume; thus, +completely decoupling the life cycle of the cloned volume from its parent at +the expense of losing some storage efficiency. Not setting +``trident.netapp.io/splitOnClone`` or setting it to ``false`` results in +reduced space consumption on the backend at the expense of creating +dependencies between the parent and clone volumes such that the parent volume +cannot be deleted unless the clone is deleted first. A scenario where splitting +the clone makes sense is cloning an empty database volume where it's expected +for the volume and its clone to greatly diverge and not benefit from storage +efficiencies offered by ONTAP. + +``sample-input/pvc-basic.yaml``, ``sample-input/pvc-basic-clone.yaml``, and +``sample-input/pvc-full.yaml`` contain examples of PVC definitions for use with +Trident. See :ref:`Trident Volume objects` for a full description of the +parameters and settings associated with Trident volumes. Kubernetes PersistentVolume objects ----------------------------------- @@ -165,6 +190,7 @@ provisioningType string thin, thick Pool supports t backendType string ontap-nas, ontap-nas-economy, Pool belongs to this type of backend Backend specified All drivers ontap-san, solidfire-san, eseries-iscsi snapshots bool true, false Pool supports volumes with snapshots Volume with snapshots enabled ontap-nas, ontap-san, solidfire-san +clones bool true, false Pool supports cloning volumes Volume with clones enabled ontap-nas, ontap-san, solidfire-san encryption bool true, false Pool supports encrypted volumes Volume with encryption enabled ontap-nas, ontap-nas-economy, ontap-san IOPS int positive integer Pool is capable of guaranteeing IOPS in this range Volume guaranteed these IOPS solidfire-san ================= ====== ======================================= ========================================================== ============================== ========================================================= @@ -264,9 +290,9 @@ determines where that volume can be provisioned, along with a size. A volume configuration defines the properties that a provisioned volume should have. -================= ====== ======== ============================================================== +================= ====== ======== ================================================================ Attribute Type Required Description -================= ====== ======== ============================================================== +================= ====== ======== ================================================================ version string no Version of the Trident API ("1") name string yes Name of volume to create storageClass string yes Storage class to use when provisioning the volume @@ -279,7 +305,9 @@ snapshotDirectory bool no ontap-nas\*: Whether the snapshot directory is unixPermissions string no ontap-nas\*: Initial UNIX permissions blockSize string no solidfire-\*: Block/sector size fileSystem string no File system type -================= ====== ======== ============================================================== +cloneSourceVolume string no ontap-{nas|san} & solidfire-\*: Name of the volume to clone from +splitOnClone string no ontap-{nas|san}: Split the clone from its parent +================= ====== ======== ================================================================ As mentioned, Trident generates ``internalName`` when creating the volume. This consists of two steps. First, it prepends the storage prefix -- either the diff --git a/frontend/docker/plugin.go b/frontend/docker/plugin.go index a037c895b..0f0ccc707 100644 --- a/frontend/docker/plugin.go +++ b/frontend/docker/plugin.go @@ -101,11 +101,10 @@ func (p *DockerPlugin) Version() string { func (p *DockerPlugin) Create(request *volume.CreateRequest) error { log.WithFields(log.Fields{ - "Method": "Create", - "Type": "DockerPlugin", + "method": "Create", "name": request.Name, "options": request.Options, - }).Debug("Create") + }).Debug("Docker frontend method is invoked.") // Find a matching storage class, or register a new one scConfig, err := getStorageClass(request.Options, p.orchestrator) @@ -120,7 +119,7 @@ func (p *DockerPlugin) Create(request *volume.CreateRequest) error { } // Invoke the orchestrator to create or clone the new volume - if volConfig.SourceName != "" { + if volConfig.CloneSourceVolume != "" { _, err = p.orchestrator.CloneVolume(volConfig) } else { _, err = p.orchestrator.AddVolume(volConfig) @@ -131,9 +130,8 @@ func (p *DockerPlugin) Create(request *volume.CreateRequest) error { func (p *DockerPlugin) List() (*volume.ListResponse, error) { log.WithFields(log.Fields{ - "Method": "List", - "Type": "DockerPlugin", - }).Debug("List") + "method": "List", + }).Debug("Docker frontend method is invoked.") tridentVols := p.orchestrator.ListVolumes() var dockerVols []*volume.Volume @@ -149,10 +147,9 @@ func (p *DockerPlugin) List() (*volume.ListResponse, error) { func (p *DockerPlugin) Get(request *volume.GetRequest) (*volume.GetResponse, error) { log.WithFields(log.Fields{ - "Method": "Get", - "Type": "DockerPlugin", + "method": "Get", "name": request.Name, - }).Debug("Get") + }).Debug("Docker frontend method is invoked") tridentVol := p.orchestrator.GetVolume(request.Name) if tridentVol == nil { @@ -177,10 +174,9 @@ func (p *DockerPlugin) Get(request *volume.GetRequest) (*volume.GetResponse, err func (p *DockerPlugin) Remove(request *volume.RemoveRequest) error { log.WithFields(log.Fields{ - "Method": "Remove", - "Type": "DockerPlugin", + "method": "Remove", "name": request.Name, - }).Debug("Remove") + }).Debug("Docker frontend method is invoked.") found, err := p.orchestrator.DeleteVolume(request.Name) if !found { @@ -192,10 +188,9 @@ func (p *DockerPlugin) Remove(request *volume.RemoveRequest) error { func (p *DockerPlugin) Path(request *volume.PathRequest) (*volume.PathResponse, error) { log.WithFields(log.Fields{ - "Method": "Path", - "Type": "DockerPlugin", + "method": "Path", "name": request.Name, - }).Debug("Path") + }).Debug("Docker frontend method is invoked.") tridentVol := p.orchestrator.GetVolume(request.Name) if tridentVol == nil { @@ -213,11 +208,10 @@ func (p *DockerPlugin) Path(request *volume.PathRequest) (*volume.PathResponse, func (p *DockerPlugin) Mount(request *volume.MountRequest) (*volume.MountResponse, error) { log.WithFields(log.Fields{ - "Method": "Mount", - "Type": "DockerPlugin", + "method": "Mount", "name": request.Name, "id": request.ID, - }).Debug("Mount") + }).Debug("Docker frontend method is invoked.") tridentVol := p.orchestrator.GetVolume(request.Name) if tridentVol == nil { @@ -240,11 +234,10 @@ func (p *DockerPlugin) Mount(request *volume.MountRequest) (*volume.MountRespons func (p *DockerPlugin) Unmount(request *volume.UnmountRequest) error { log.WithFields(log.Fields{ - "Method": "Unmount", - "Type": "DockerPlugin", + "method": "Unmount", "name": request.Name, "id": request.ID, - }).Debug("Unmount") + }).Debug("Docker frontend method is invoked.") tridentVol := p.orchestrator.GetVolume(request.Name) if tridentVol == nil { @@ -265,9 +258,8 @@ func (p *DockerPlugin) Unmount(request *volume.UnmountRequest) error { func (p *DockerPlugin) Capabilities() *volume.CapabilitiesResponse { log.WithFields(log.Fields{ - "Method": "Capabilities", - "Type": "DockerPlugin", - }).Debug("Capabilities") + "method": "Capabilities", + }).Debug("Docker frontend method is invoked.") return &volume.CapabilitiesResponse{Capabilities: volume.Capability{Scope: "global"}} } diff --git a/frontend/docker/volumes.go b/frontend/docker/volumes.go index b941deed1..874ba5d90 100644 --- a/frontend/docker/volumes.go +++ b/frontend/docker/volumes.go @@ -72,7 +72,7 @@ func makeStorageClass(options map[string]string, o core.Orchestrator) (*storage_ scConfig.Attributes = make(map[string]storage_attribute.Request) for k, v := range options { // format: attribute: "type:value" - req, err := storage_attribute.CreateAttributeRequestFromTypedValue(k, v) + req, err := storage_attribute.CreateAttributeRequestFromAttributeValue(k, v) if err != nil { log.WithFields(log.Fields{ "storageClass": scConfig.Name, @@ -108,22 +108,22 @@ func getVolumeConfig(name, storageClass string, opts map[string]string) (*storag delete(opts, "size") return &storage.VolumeConfig{ - Name: name, - Size: fmt.Sprintf("%d", sizeBytes), - StorageClass: storageClass, - Protocol: config.ProtocolAny, - AccessMode: config.ModeAny, - SpaceReserve: dvp_utils.GetV(opts, "spaceReserve", ""), - SecurityStyle: dvp_utils.GetV(opts, "securityStyle", ""), - SplitOnClone: dvp_utils.GetV(opts, "splitOnClone", ""), - SnapshotPolicy: dvp_utils.GetV(opts, "snapshotPolicy", ""), - ExportPolicy: dvp_utils.GetV(opts, "exportPolicy", ""), - SnapshotDir: dvp_utils.GetV(opts, "snapshotDir", ""), - UnixPermissions: dvp_utils.GetV(opts, "unixPermissions", ""), - BlockSize: dvp_utils.GetV(opts, "blocksize", ""), - FileSystem: dvp_utils.GetV(opts, "fstype|fileSystemType", ""), - Encryption: dvp_utils.GetV(opts, "encryption", ""), - SourceName: dvp_utils.GetV(opts, "from", ""), - SourceSnapshotName: dvp_utils.GetV(opts, "fromSnapshot", ""), + Name: name, + Size: fmt.Sprintf("%d", sizeBytes), + StorageClass: storageClass, + Protocol: config.ProtocolAny, + AccessMode: config.ModeAny, + SpaceReserve: dvp_utils.GetV(opts, "spaceReserve", ""), + SecurityStyle: dvp_utils.GetV(opts, "securityStyle", ""), + SplitOnClone: dvp_utils.GetV(opts, "splitOnClone", ""), + SnapshotPolicy: dvp_utils.GetV(opts, "snapshotPolicy", ""), + ExportPolicy: dvp_utils.GetV(opts, "exportPolicy", ""), + SnapshotDir: dvp_utils.GetV(opts, "snapshotDir", ""), + UnixPermissions: dvp_utils.GetV(opts, "unixPermissions", ""), + BlockSize: dvp_utils.GetV(opts, "blocksize", ""), + FileSystem: dvp_utils.GetV(opts, "fstype|fileSystemType", ""), + Encryption: dvp_utils.GetV(opts, "encryption", ""), + CloneSourceVolume: dvp_utils.GetV(opts, "from", ""), + CloneSourceSnapshot: dvp_utils.GetV(opts, "fromSnapshot", ""), }, nil } diff --git a/frontend/kubernetes/config.go b/frontend/kubernetes/config.go index d3d39cdbb..8d53a8ae9 100644 --- a/frontend/kubernetes/config.go +++ b/frontend/kubernetes/config.go @@ -32,6 +32,8 @@ const ( AnnExportPolicy = AnnPrefix + "/exportPolicy" AnnBlockSize = AnnPrefix + "/blockSize" AnnFileSystem = AnnPrefix + "/fileSystem" + AnnCloneFromPVC = AnnPrefix + "/cloneFromPVC" + AnnSplitOnClone = AnnPrefix + "/splitOnClone" // Minimum and maximum supported Kubernetes versions KubernetesVersionMin = "v1.4.0" diff --git a/frontend/kubernetes/plugin.go b/frontend/kubernetes/plugin.go index 3377420a7..0e11e7bf5 100644 --- a/frontend/kubernetes/plugin.go +++ b/frontend/kubernetes/plugin.go @@ -636,8 +636,59 @@ func (p *KubernetesPlugin) createVolumeAndPV(uniqueName string, annotations[AnnClass] = GetPersistentVolumeClaimClass(claim) } - vol, err = p.orchestrator.AddVolume( - getVolumeConfig(accessModes, uniqueName, size, annotations)) + k8sClient, newKubeClientErr := k8s_client.NewKubeClient(&p.kubeConfig, claim.Namespace) + if newKubeClientErr != nil { + log.WithFields(log.Fields{ + "claim.Namespace": claim.Namespace, + }).Warnf("Kubernetes frontend couldn't create a client to namespace: %v error: %v", + claim.Namespace, newKubeClientErr.Error()) + } + + // Create the volume configuration object + volConfig := getVolumeConfig(accessModes, uniqueName, size, annotations) + if volConfig.CloneSourceVolume == "" { + vol, err = p.orchestrator.AddVolume(volConfig) + } else { + var ( + options metav1.GetOptions + pvc *v1.PersistentVolumeClaim + ) + + // If cloning an existing PVC, process the source PVC name: + // 1) Validate that the source PVC is in the same namespace. + // TODO: Explore the security and management ramifications of cloning + // from a PVC in a different namespace. + if pvc, err = k8sClient.GetPVC(volConfig.CloneSourceVolume, options); err != nil { + err = fmt.Errorf("Cloning from a PVC requires both PVCs be in " + + "the same namespace!") + log.WithFields(log.Fields{ + "sourcePVC": volConfig.CloneSourceVolume, + "PVC": claim.Name, + "PVC_namespace": claim.Namespace, + }).Debugf("Kubernetes frontend detected an invalid configuration "+ + "for cloning from a PVC: %v", err.Error()) + return + } + + // 2) Validate that storage classes match for the two PVCs + if GetPersistentVolumeClaimClass(pvc) != GetPersistentVolumeClaimClass(claim) { + err = fmt.Errorf("Cloning from a PVC requires matching storage classes!") + log.WithFields(log.Fields{ + "PVC": claim.Name, + "PVC_storageClass": GetPersistentVolumeClaimClass(claim), + "sourcePVC": volConfig.CloneSourceVolume, + "sourcePVC_storageClass": GetPersistentVolumeClaimClass(pvc), + }).Debugf("Kubernetes frontend detected an invalid configuration "+ + "for cloning from a PVC: %v", err.Error()) + return + } + + // 3) Set the source PVC name as it's understood by Trident. + volConfig.CloneSourceVolume = getUniqueClaimName(pvc) + + // 4) Clone the existing volume + vol, err = p.orchestrator.CloneVolume(volConfig) + } if err != nil { log.WithFields(log.Fields{ "volume": uniqueName, @@ -683,14 +734,6 @@ func (p *KubernetesPlugin) createVolumeAndPV(uniqueName string, v1.PersistentVolumeReclaimRetain } - k8sClient, newKubeClientErr := k8s_client.NewKubeClient(&p.kubeConfig, claim.Namespace) - if newKubeClientErr != nil { - log.WithFields(log.Fields{ - "claim.Namespace": claim.Namespace, - }).Warnf("Kubernetes frontend couldn't create a client to namespace: %v error: %v", - claim.Namespace, newKubeClientErr.Error()) - } - driverType := p.orchestrator.GetDriverTypeForVolume(vol) switch { case driverType == dvp.SolidfireSANStorageDriverName || @@ -1049,7 +1092,7 @@ func (p *KubernetesPlugin) processAddedClass(class *k8s_storage_v1.StorageClass) for k, v := range class.Parameters { if k == storage_attribute.BackendStoragePools { // format: backendStoragePools: "backend1:pool1,pool2;backend2:pool1" - backendVCs, err := storage_attribute.CreateBackendStoragePoolsMapFromEncodedString(v) + backendPools, err := storage_attribute.CreateBackendStoragePoolsMapFromEncodedString(v) if err != nil { log.WithFields(log.Fields{ "storageClass": class.Name, @@ -1058,11 +1101,11 @@ func (p *KubernetesPlugin) processAddedClass(class *k8s_storage_v1.StorageClass) }).Error("Kubernetes frontend couldn't process %s parameter: ", storage_attribute.BackendStoragePools, err) } - scConfig.BackendStoragePools = backendVCs + scConfig.BackendStoragePools = backendPools continue } - // format: attribute: "type:value" - req, err := storage_attribute.CreateAttributeRequestFromTypedValue(k, v) + // format: attribute: "value" + req, err := storage_attribute.CreateAttributeRequestFromAttributeValue(k, v) if err != nil { log.WithFields(log.Fields{ "storageClass": class.Name, diff --git a/frontend/kubernetes/volumes.go b/frontend/kubernetes/volumes.go index 124342992..41e46cec4 100644 --- a/frontend/kubernetes/volumes.go +++ b/frontend/kubernetes/volumes.go @@ -73,6 +73,7 @@ func getVolumeConfig( annotations map[string]string, ) *storage.VolumeConfig { var accessMode config.AccessMode + if len(accessModes) > 1 { accessMode = config.ReadWriteMany } else if len(accessModes) == 0 { @@ -80,21 +81,25 @@ func getVolumeConfig( } else { accessMode = config.AccessMode(accessModes[0]) } + if getAnnotation(annotations, AnnFileSystem) == "" { annotations[AnnFileSystem] = "ext4" } + return &storage.VolumeConfig{ - Name: name, - Size: fmt.Sprintf("%d", size.Value()), - Protocol: config.Protocol(getAnnotation(annotations, AnnProtocol)), - SnapshotPolicy: getAnnotation(annotations, AnnSnapshotPolicy), - ExportPolicy: getAnnotation(annotations, AnnExportPolicy), - SnapshotDir: getAnnotation(annotations, AnnSnapshotDir), - UnixPermissions: getAnnotation(annotations, AnnUnixPermissions), - StorageClass: getAnnotation(annotations, AnnClass), - BlockSize: getAnnotation(annotations, AnnBlockSize), - FileSystem: getAnnotation(annotations, AnnFileSystem), - AccessMode: accessMode, + Name: name, + Size: fmt.Sprintf("%d", size.Value()), + Protocol: config.Protocol(getAnnotation(annotations, AnnProtocol)), + SnapshotPolicy: getAnnotation(annotations, AnnSnapshotPolicy), + ExportPolicy: getAnnotation(annotations, AnnExportPolicy), + SnapshotDir: getAnnotation(annotations, AnnSnapshotDir), + UnixPermissions: getAnnotation(annotations, AnnUnixPermissions), + StorageClass: getAnnotation(annotations, AnnClass), + BlockSize: getAnnotation(annotations, AnnBlockSize), + FileSystem: getAnnotation(annotations, AnnFileSystem), + CloneSourceVolume: getAnnotation(annotations, AnnCloneFromPVC), + SplitOnClone: getAnnotation(annotations, AnnSplitOnClone), + AccessMode: accessMode, } } diff --git a/storage/backend.go b/storage/backend.go index dcde62a66..5b5a0ffb4 100644 --- a/storage/backend.go +++ b/storage/backend.go @@ -171,8 +171,8 @@ func (b *StorageBackend) CloneVolume( log.WithFields(log.Fields{ "storagePool": storagePool.Name, "storageClass": volConfig.StorageClass, - "sourceVolume": volConfig.SourceName, - "sourceSnapshot": volConfig.SourceSnapshotName, + "sourceVolume": volConfig.CloneSourceVolume, + "sourceSnapshot": volConfig.CloneSourceSnapshot, "cloneVolume": volConfig.Name, }).Debug("Attempting volume clone.") @@ -193,7 +193,8 @@ func (b *StorageBackend) CloneVolume( } err = b.Driver.CreateClone(volConfig.InternalName, - volConfig.SourceInternalName, volConfig.SourceSnapshotName, args) + volConfig.CloneSourceVolumeInternal, volConfig.CloneSourceSnapshot, + args) if err != nil { return nil, err } diff --git a/storage/eseries/eseries.go b/storage/eseries/eseries.go index 0cb91f8eb..324b9b099 100644 --- a/storage/eseries/eseries.go +++ b/storage/eseries/eseries.go @@ -55,8 +55,9 @@ func (d *EseriesStorageDriver) GetStorageBackendSpecs(backend *storage.StorageBa vc.Attributes[sa.Media] = sa.NewStringOffer(sa.SSD) } - // No snapshots or thin provisioning on E-series + // No snapshots, clones or thin provisioning on E-series vc.Attributes[sa.Snapshots] = sa.NewBoolOffer(false) + vc.Attributes[sa.Clones] = sa.NewBoolOffer(false) vc.Attributes[sa.Encryption] = sa.NewBoolOffer(false) vc.Attributes[sa.ProvisioningType] = sa.NewStringOffer("thick") diff --git a/storage/fake/fake.go b/storage/fake/fake.go index 82fd85674..1cae80075 100644 --- a/storage/fake/fake.go +++ b/storage/fake/fake.go @@ -50,8 +50,9 @@ func (d *FakeStorageDriver) GetInternalVolumeName(name string) string { func (d *FakeStorageDriver) CreatePrepare(volConfig *storage.VolumeConfig) bool { volConfig.InternalName = d.GetInternalVolumeName(volConfig.Name) - if volConfig.SourceName != "" { - volConfig.SourceInternalName = d.GetInternalVolumeName(volConfig.SourceName) + if volConfig.CloneSourceVolume != "" { + volConfig.CloneSourceVolumeInternal = + d.GetInternalVolumeName(volConfig.CloneSourceVolume) } return true } diff --git a/storage/ontap/ontap_common.go b/storage/ontap/ontap_common.go index d32e941bd..8b683a2dc 100644 --- a/storage/ontap/ontap_common.go +++ b/storage/ontap/ontap_common.go @@ -296,8 +296,9 @@ func createPrepareCommon(d storage.TridentDriver, volConfig *storage.VolumeConfi volConfig.InternalName = d.GetInternalVolumeName(volConfig.Name) - if volConfig.SourceName != "" { - volConfig.SourceInternalName = d.GetInternalVolumeName(volConfig.SourceName) + if volConfig.CloneSourceVolume != "" { + volConfig.CloneSourceVolumeInternal = + d.GetInternalVolumeName(volConfig.CloneSourceVolume) } return true diff --git a/storage/ontap/ontap_nas.go b/storage/ontap/ontap_nas.go index 27bc8e2f5..eca5118d8 100644 --- a/storage/ontap/ontap_nas.go +++ b/storage/ontap/ontap_nas.go @@ -31,6 +31,7 @@ func (d *OntapNASStorageDriver) GetStoragePoolAttributes() map[string]sa.Offer { return map[string]sa.Offer{ sa.BackendType: sa.NewStringOffer(d.Name()), sa.Snapshots: sa.NewBoolOffer(true), + sa.Clones: sa.NewBoolOffer(true), sa.Encryption: sa.NewBoolOffer(d.API.SupportsApiFeature(ontap.NETAPP_VOLUME_ENCRYPTION)), sa.ProvisioningType: sa.NewStringOffer("thick", "thin"), } diff --git a/storage/ontap/ontap_nas_qtree.go b/storage/ontap/ontap_nas_qtree.go index 84099b4ba..1ad451f92 100644 --- a/storage/ontap/ontap_nas_qtree.go +++ b/storage/ontap/ontap_nas_qtree.go @@ -31,6 +31,7 @@ func (d *OntapNASQtreeStorageDriver) GetStoragePoolAttributes() map[string]sa.Of return map[string]sa.Offer{ sa.BackendType: sa.NewStringOffer(d.Name()), sa.Snapshots: sa.NewBoolOffer(false), + sa.Clones: sa.NewBoolOffer(false), sa.Encryption: sa.NewBoolOffer(d.API.SupportsApiFeature(ontap.NETAPP_VOLUME_ENCRYPTION)), sa.ProvisioningType: sa.NewStringOffer("thick", "thin"), } diff --git a/storage/ontap/ontap_san.go b/storage/ontap/ontap_san.go index b05ff529f..ae913c3f2 100644 --- a/storage/ontap/ontap_san.go +++ b/storage/ontap/ontap_san.go @@ -34,6 +34,7 @@ func (d *OntapSANStorageDriver) GetStoragePoolAttributes() map[string]sa.Offer { return map[string]sa.Offer{ sa.BackendType: sa.NewStringOffer(d.Name()), sa.Snapshots: sa.NewBoolOffer(true), + sa.Clones: sa.NewBoolOffer(true), sa.Encryption: sa.NewBoolOffer(d.API.SupportsApiFeature(ontap.NETAPP_VOLUME_ENCRYPTION)), sa.ProvisioningType: sa.NewStringOffer("thick", "thin"), } diff --git a/storage/solidfire/solidfire_san.go b/storage/solidfire/solidfire_san.go index dd7815af2..6da41c329 100644 --- a/storage/solidfire/solidfire_san.go +++ b/storage/solidfire/solidfire_san.go @@ -64,6 +64,7 @@ func (d *SolidfireSANStorageDriver) GetStorageBackendSpecs( vc.Attributes[sa.IOPS] = sa.NewIntOffer(int(volType.QOS.MinIOPS), int(volType.QOS.MaxIOPS)) vc.Attributes[sa.Snapshots] = sa.NewBoolOffer(true) + vc.Attributes[sa.Clones] = sa.NewBoolOffer(true) vc.Attributes[sa.Encryption] = sa.NewBoolOffer(false) vc.Attributes[sa.ProvisioningType] = sa.NewStringOffer("thin") vc.Attributes[sa.BackendType] = sa.NewStringOffer(d.Name()) @@ -86,8 +87,9 @@ func (d *SolidfireSANStorageDriver) CreatePrepare( // 1. Sanitize the volume name volConfig.InternalName = d.GetInternalVolumeName(volConfig.Name) - if volConfig.SourceName != "" { - volConfig.SourceInternalName = d.GetInternalVolumeName(volConfig.SourceName) + if volConfig.CloneSourceVolume != "" { + volConfig.CloneSourceVolumeInternal = + d.GetInternalVolumeName(volConfig.CloneSourceVolume) } return true diff --git a/storage/volume.go b/storage/volume.go index 883fe8889..840f51e9c 100644 --- a/storage/volume.go +++ b/storage/volume.go @@ -12,27 +12,27 @@ import ( ) type VolumeConfig struct { - Version string `json:"version"` - Name string `json:"name"` - InternalName string `json:"internalName"` - Size string `json:"size"` - Protocol config.Protocol `json:"protocol"` - SpaceReserve string `json:"spaceReserve"` - SecurityStyle string `json:"securityStyle"` - SplitOnClone string `json:"splitOnClone"` - SnapshotPolicy string `json:"snapshotPolicy,omitempty"` - ExportPolicy string `json:"exportPolicy,omitempty"` - SnapshotDir string `json:"snapshotDirectory,omitempty"` - UnixPermissions string `json:"unixPermissions,omitempty"` - StorageClass string `json:"storageClass,omitempty"` - AccessMode config.AccessMode `json:"accessMode,omitempty"` - AccessInfo VolumeAccessInfo `json:"accessInformation"` - BlockSize string `json:"blockSize"` - FileSystem string `json:"fileSystem"` - Encryption string `json:"encryption"` - SourceName string `json:"sourceName"` - SourceInternalName string `json:"sourceInternalName"` - SourceSnapshotName string `json:"sourceSnapshotName"` + Version string `json:"version"` + Name string `json:"name"` + InternalName string `json:"internalName"` + Size string `json:"size"` + Protocol config.Protocol `json:"protocol"` + SpaceReserve string `json:"spaceReserve"` + SecurityStyle string `json:"securityStyle"` + SnapshotPolicy string `json:"snapshotPolicy,omitempty"` + ExportPolicy string `json:"exportPolicy,omitempty"` + SnapshotDir string `json:"snapshotDirectory,omitempty"` + UnixPermissions string `json:"unixPermissions,omitempty"` + StorageClass string `json:"storageClass,omitempty"` + AccessMode config.AccessMode `json:"accessMode,omitempty"` + AccessInfo VolumeAccessInfo `json:"accessInformation"` + BlockSize string `json:"blockSize"` + FileSystem string `json:"fileSystem"` + Encryption string `json:"encryption"` + CloneSourceVolume string `json:"cloneSourceVolume"` + CloneSourceVolumeInternal string `json:"cloneSourceVolumeInternal"` + CloneSourceSnapshot string `json:"cloneSourceSnapshot"` + SplitOnClone string `json:"splitOnClone"` } type VolumeAccessInfo struct { diff --git a/storage_attribute/common_attributes.go b/storage_attribute/common_attributes.go index 2d296ef96..1c7dc0d57 100644 --- a/storage_attribute/common_attributes.go +++ b/storage_attribute/common_attributes.go @@ -8,6 +8,7 @@ const ( // Constants for boolean storage category attributes Snapshots = "snapshots" + Clones = "clones" Encryption = "encryption" // Constants for string list attributes @@ -32,6 +33,7 @@ const ( var attrTypes = map[string]StorageAttributeType{ IOPS: intType, Snapshots: boolType, + Clones: boolType, Encryption: boolType, ProvisioningType: stringType, BackendType: stringType, diff --git a/storage_attribute/request.go b/storage_attribute/request.go index b33c99f83..171ab322f 100644 --- a/storage_attribute/request.go +++ b/storage_attribute/request.go @@ -23,7 +23,7 @@ func UnmarshalRequestMap(mapJSON json.RawMessage) ( return nil, fmt.Errorf("Unable to unmarshal map: %v", err) } for name, stringVal := range tmp { - ret[name], err = CreateAttributeRequestFromTypedValue(name, stringVal) + ret[name], err = CreateAttributeRequestFromAttributeValue(name, stringVal) if err != nil { return nil, err } @@ -42,7 +42,7 @@ func MarshalRequestMap(requestMap map[string]Request) ([]byte, error) { return json.Marshal(genericMap) } -func CreateAttributeRequestFromTypedValue(name, val string) (Request, error) { +func CreateAttributeRequestFromAttributeValue(name, val string) (Request, error) { var req Request valType, ok := attrTypes[name] if !ok { diff --git a/storage_class/test_utils/utils.go b/storage_class/test_utils/utils.go index 89df4772a..8f7833115 100644 --- a/storage_class/test_utils/utils.go +++ b/storage_class/test_utils/utils.go @@ -24,8 +24,8 @@ type PoolMatch struct { Pool string } -func (p *PoolMatch) Matches(vc *storage.StoragePool) bool { - return vc.Name == p.Pool && vc.Backend.Name == p.Backend +func (p *PoolMatch) Matches(pool *storage.StoragePool) bool { + return pool.Name == p.Pool && pool.Backend.Name == p.Backend } func (p *PoolMatch) String() string { diff --git a/trident-installer/sample-input/pvc-basic-v1.yaml b/trident-installer/sample-input/pvc-basic-beta.yaml similarity index 70% rename from trident-installer/sample-input/pvc-basic-v1.yaml rename to trident-installer/sample-input/pvc-basic-beta.yaml index c32d93ec4..44173a586 100644 --- a/trident-installer/sample-input/pvc-basic-v1.yaml +++ b/trident-installer/sample-input/pvc-basic-beta.yaml @@ -2,10 +2,11 @@ kind: PersistentVolumeClaim apiVersion: v1 metadata: name: basic + annotations: + volume.beta.kubernetes.io/storage-class: basic spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi - storageClassName: basic diff --git a/trident-installer/sample-input/pvc-basic-clone.yaml b/trident-installer/sample-input/pvc-basic-clone.yaml new file mode 100644 index 000000000..d3bc931f3 --- /dev/null +++ b/trident-installer/sample-input/pvc-basic-clone.yaml @@ -0,0 +1,13 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: basicclone + annotations: + trident.netapp.io/cloneFromPVC: basic +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: basic diff --git a/trident-installer/sample-input/pvc-basic.yaml b/trident-installer/sample-input/pvc-basic.yaml index 44173a586..c32d93ec4 100644 --- a/trident-installer/sample-input/pvc-basic.yaml +++ b/trident-installer/sample-input/pvc-basic.yaml @@ -2,11 +2,10 @@ kind: PersistentVolumeClaim apiVersion: v1 metadata: name: basic - annotations: - volume.beta.kubernetes.io/storage-class: basic spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi + storageClassName: basic diff --git a/trident-installer/sample-input/storage-class-basic-v1.yaml.templ b/trident-installer/sample-input/storage-class-basic-v1beta1.yaml.templ similarity index 77% rename from trident-installer/sample-input/storage-class-basic-v1.yaml.templ rename to trident-installer/sample-input/storage-class-basic-v1beta1.yaml.templ index f0cce29d4..57763c1c7 100644 --- a/trident-installer/sample-input/storage-class-basic-v1.yaml.templ +++ b/trident-installer/sample-input/storage-class-basic-v1beta1.yaml.templ @@ -1,4 +1,4 @@ -apiVersion: storage.k8s.io/v1 +apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: name: basic diff --git a/trident-installer/sample-input/storage-class-basic.yaml.templ b/trident-installer/sample-input/storage-class-basic.yaml.templ index 57763c1c7..f0cce29d4 100644 --- a/trident-installer/sample-input/storage-class-basic.yaml.templ +++ b/trident-installer/sample-input/storage-class-basic.yaml.templ @@ -1,4 +1,4 @@ -apiVersion: storage.k8s.io/v1beta1 +apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: basic diff --git a/trident-installer/sample-input/storage-class-ontap-gold.yaml b/trident-installer/sample-input/storage-class-ontap-gold.yaml index 62cad664e..b55f4f3bb 100644 --- a/trident-installer/sample-input/storage-class-ontap-gold.yaml +++ b/trident-installer/sample-input/storage-class-ontap-gold.yaml @@ -1,4 +1,4 @@ -apiVersion: storage.k8s.io/v1beta1 +apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: ontap-gold diff --git a/trident-installer/sample-input/storage-class-solidfire-bronze.yaml b/trident-installer/sample-input/storage-class-solidfire-bronze.yaml index 7e1d2a7f5..5cec7bbdf 100644 --- a/trident-installer/sample-input/storage-class-solidfire-bronze.yaml +++ b/trident-installer/sample-input/storage-class-solidfire-bronze.yaml @@ -1,4 +1,4 @@ -apiVersion: storage.k8s.io/v1beta1 +apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: solidfire-bronze