Skip to content
Closed
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
12 changes: 12 additions & 0 deletions frontend/csi/controller_helpers/kubernetes/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ const (
AnnTieringPolicy = prefix + "/tieringPolicy"
AnnTieringMinimumCoolingDays = prefix + "/tieringMinimumCoolingDays"

// Shift/MTV StorageClass annotations
AnnShiftStorageClassType = "shift.netapp.io/storage-class-type"
AnnShiftTridentBackendUUID = "shift.netapp.io/trident-backend-uuid"
AnnShiftEndpoint = "shift.netapp.io/endpoint"

// MTV PVC annotations
AnnMTVDiskPath = "mtv.redhat.com/disk-path"
AnnMTVNFSServer = "mtv.redhat.com/nfs-server"
AnnMTVNFSPath = "mtv.redhat.com/nfs-path"
AnnMTVVMID = "mtv.redhat.com/vm-id"
AnnMTVVMUUID = "mtv.redhat.com/vm-uuid"

// Pod remediation policy annotation and values
AnnPodRemediationPolicyAnnotation = prefix + "/podRemediationPolicy"
PodRemediationPolicyDelete = "delete"
Expand Down
164 changes: 164 additions & 0 deletions frontend/csi/controller_helpers/kubernetes/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package kubernetes

import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
Expand Down Expand Up @@ -99,6 +100,32 @@ func (h *helper) GetVolumeConfig(

volumeConfig := getVolumeConfig(ctx, pvc, pvName, pvcSize, annotations, sc, requisiteTopology, preferredTopology)

// Detect Shift StorageClass and populate ShiftConfig with ONTAP credentials + MTV metadata
if scAnnotations[AnnShiftStorageClassType] == "shift" {
Logc(ctx).WithFields(LogFields{
"storageClass": sc.Name,
"pvc": pvc.Name,
}).Info("Shift StorageClass detected, resolving ONTAP credentials for Shift integration.")

shiftCfg, shiftErr := h.buildShiftConfig(ctx, pvc, scAnnotations)
if shiftErr != nil {
return nil, fmt.Errorf("failed to build Shift config for PVC %s: %v", pvc.Name, shiftErr)
}
volumeConfig.Shift = shiftCfg

Logc(ctx).WithFields(LogFields{
"endpoint": shiftCfg.Endpoint,
"backendUUID": shiftCfg.BackendUUID,
"managementLIF": shiftCfg.ManagementLIF,
"svm": shiftCfg.SVM,
"diskPath": shiftCfg.DiskPath,
"nfsServer": shiftCfg.NFSServer,
"nfsPath": shiftCfg.NFSPath,
"vmID": shiftCfg.VMID,
"vmUUID": shiftCfg.VMUUID,
}).Info("Shift config populated on VolumeConfig.")
}

// Update the volume config with the Access Control only if the storage class nasType parameter is SMB
if sc.Parameters[SCParameterNASType] == NASTypeSMB {
err = h.updateVolumeConfigWithSecureSMBAccessControl(ctx, volumeConfig, sc, annotations, scAnnotations, secrets)
Expand Down Expand Up @@ -965,6 +992,143 @@ func processSCAnnotations(sc *k8sstoragev1.StorageClass) map[string]string {
return annotations
}

// buildShiftConfig resolves all information needed for the Shift integration:
// ONTAP credentials from the TBC secret, MTV metadata from PVC annotations,
// and the Shift endpoint from StorageClass annotations.
func (h *helper) buildShiftConfig(
ctx context.Context,
pvc *v1.PersistentVolumeClaim,
scAnnotations map[string]string,
) (*storage.ShiftConfig, error) {
endpoint := scAnnotations[AnnShiftEndpoint]
if endpoint == "" {
return nil, fmt.Errorf("StorageClass missing %s annotation", AnnShiftEndpoint)
}
backendUUID := scAnnotations[AnnShiftTridentBackendUUID]
if backendUUID == "" {
return nil, fmt.Errorf("StorageClass missing %s annotation", AnnShiftTridentBackendUUID)
}

pvcAnn := pvc.Annotations
diskPath := pvcAnn[AnnMTVDiskPath]
nfsServer := pvcAnn[AnnMTVNFSServer]
nfsPath := pvcAnn[AnnMTVNFSPath]

if diskPath == "" || nfsServer == "" || nfsPath == "" {
return nil, fmt.Errorf("PVC %s missing required MTV annotations (disk-path, nfs-server, nfs-path)", pvc.Name)
}

// Resolve ONTAP connection info from the backend
mgmtLIF, svm, username, password, err := h.resolveOntapCredentials(ctx, backendUUID)
if err != nil {
return nil, fmt.Errorf("failed to resolve ONTAP credentials for backend %s: %v", backendUUID, err)
}

return &storage.ShiftConfig{
Endpoint: endpoint,
BackendUUID: backendUUID,
ManagementLIF: mgmtLIF,
SVM: svm,
Username: username,
Password: password,
DiskPath: diskPath,
NFSServer: nfsServer,
NFSPath: nfsPath,
VMID: pvcAnn[AnnMTVVMID],
VMUUID: pvcAnn[AnnMTVVMUUID],
}, nil
}

// resolveOntapCredentials fetches the ManagementLIF and SVM from the BackendExternal,
// then reads the ONTAP username/password from the Kubernetes Secret referenced by the TBC.
func (h *helper) resolveOntapCredentials(
ctx context.Context, backendUUID string,
) (mgmtLIF, svm, username, password string, err error) {

backendExt, err := h.orchestrator.GetBackendByBackendUUID(ctx, backendUUID)
if err != nil {
return "", "", "", "", fmt.Errorf("backend %s not found: %v", backendUUID, err)
}

Logc(ctx).WithFields(LogFields{
"backendName": backendExt.Name,
"backendUUID": backendExt.BackendUUID,
"configRef": backendExt.ConfigRef,
}).Debug("Shift: resolved backend for credential lookup.")

// Extract non-sensitive fields (ManagementLIF, SVM) from the external config.
// The external config is a map[string]interface{} when JSON-round-tripped.
configJSON, jsonErr := json.Marshal(backendExt.Config)
if jsonErr != nil {
return "", "", "", "", fmt.Errorf("cannot marshal backend config: %v", jsonErr)
}
var parsed map[string]interface{}
if jsonErr = json.Unmarshal(configJSON, &parsed); jsonErr != nil {
return "", "", "", "", fmt.Errorf("cannot unmarshal backend config: %v", jsonErr)
}

if v, ok := parsed["managementLIF"].(string); ok {
mgmtLIF = v
}
if v, ok := parsed["svm"].(string); ok {
svm = v
}

Logc(ctx).WithFields(LogFields{
"managementLIF": mgmtLIF,
"svm": svm,
}).Debug("Shift: extracted ONTAP connection info from backend config.")

// Look up the TBC CRD to find the credentials secret reference
configRef := backendExt.ConfigRef
if configRef == "" {
return "", "", "", "", fmt.Errorf("backend %s has no configRef (TBC)", backendUUID)
}

tbc, tbcErr := h.tridentClient.TridentV1().TridentBackendConfigs(h.namespace).Get(ctx, configRef, getOpts)
if tbcErr != nil {
return "", "", "", "", fmt.Errorf("failed to get TBC %s: %v", configRef, tbcErr)
}

var tbcSpec map[string]interface{}
if jsonErr = json.Unmarshal(tbc.Spec.Raw, &tbcSpec); jsonErr != nil {
return "", "", "", "", fmt.Errorf("failed to parse TBC spec: %v", jsonErr)
}

credsRaw, ok := tbcSpec["credentials"]
if !ok {
return "", "", "", "", fmt.Errorf("TBC %s has no credentials field", configRef)
}
credsMap, ok := credsRaw.(map[string]interface{})
if !ok {
return "", "", "", "", fmt.Errorf("TBC %s credentials field is not a map", configRef)
}

secretName, _ := credsMap["name"].(string)
if secretName == "" {
return "", "", "", "", fmt.Errorf("TBC %s credentials missing secret name", configRef)
}

Logc(ctx).WithFields(LogFields{
"secretName": secretName,
"namespace": h.namespace,
}).Debug("Shift: reading credentials secret.")

secret, secretErr := h.kubeClient.CoreV1().Secrets(h.namespace).Get(ctx, secretName, getOpts)
if secretErr != nil {
return "", "", "", "", fmt.Errorf("failed to read secret %s/%s: %v", h.namespace, secretName, secretErr)
}

username = string(secret.Data["username"])
password = string(secret.Data["password"])
if username == "" || password == "" {
return "", "", "", "", fmt.Errorf("secret %s missing username or password", secretName)
}

Logc(ctx).Debug("Shift: successfully resolved ONTAP credentials from TBC secret.")
return mgmtLIF, svm, username, password, nil
}

// getSMBShareAccessControlFromPVCAnnotation parses the smbShareAccessControl annotation and updates the smbShareACL map
func getSMBShareAccessControlFromPVCAnnotation(smbShareAccessControlAnn string) (map[string]string, error) {
// Structure to hold the parsed smbShareAccessControlAnnotation
Expand Down
71 changes: 71 additions & 0 deletions frontend/csi/controller_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

tridentconfig "github.com/netapp/trident/config"
controllerhelpers "github.com/netapp/trident/frontend/csi/controller_helpers"
"github.com/netapp/trident/frontend/csi/shift"
. "github.com/netapp/trident/logging"
"github.com/netapp/trident/pkg/capacity"
"github.com/netapp/trident/pkg/collection"
Expand Down Expand Up @@ -239,6 +240,76 @@ func (p *Plugin) CreateVolume(
return nil, p.getCSIErrorForOrchestratorError(err)
}

// --- Shift integration: intercept before clone/import/create decision ---
// Only triggered when StorageClass has annotation shift.netapp.io/storage-class-type: "shift".
// Normal PVCs (without that annotation) will have volConfig.Shift == nil and skip this block entirely.
if volConfig.Shift != nil {
Logc(ctx).WithFields(LogFields{
"pvcName": volConfig.RequestName,
"pvcNamespace": volConfig.Namespace,
"storageClass": volConfig.StorageClass,
"endpoint": volConfig.Shift.Endpoint,
"backendUUID": volConfig.Shift.BackendUUID,
"managementLIF": volConfig.Shift.ManagementLIF,
"svm": volConfig.Shift.SVM,
"hasUsername": volConfig.Shift.Username != "",
"hasPassword": volConfig.Shift.Password != "",
"diskPath": volConfig.Shift.DiskPath,
"nfsServer": volConfig.Shift.NFSServer,
"nfsPath": volConfig.Shift.NFSPath,
"vmID": volConfig.Shift.VMID,
"vmUUID": volConfig.Shift.VMUUID,
}).Info("Shift: PVC targets a Shift StorageClass -- entering Shift flow.")

shiftResp, shiftErr := p.shiftClient.InvokeShiftJob(ctx, volConfig.Shift, req.Name, volConfig.Namespace)
if shiftErr != nil {
msg := fmt.Sprintf("Shift API call failed for PVC %s: %v", req.Name, shiftErr)
Logc(ctx).Error(msg)
p.controllerHelper.RecordVolumeEvent(ctx, req.Name, controllerhelpers.EventTypeWarning,
"ShiftJobFailed", msg)
return nil, status.Error(codes.Internal, msg)
}

switch shiftResp.Status {
case shift.JobStatusSuccess:
Logc(ctx).WithFields(LogFields{
"volumeName": shiftResp.VolumeName,
"jobID": shiftResp.JobID,
}).Info("Shift: job succeeded, converting to volume import.")

volConfig.ImportOriginalName = shiftResp.VolumeName
volConfig.ImportBackendUUID = volConfig.Shift.BackendUUID
volConfig.ImportNotManaged = false

p.controllerHelper.RecordVolumeEvent(ctx, req.Name, controllerhelpers.EventTypeNormal,
"ShiftJobSucceeded",
fmt.Sprintf("Shift job completed, importing volume %s", shiftResp.VolumeName))

case shift.JobStatusRunning:
Logc(ctx).WithFields(LogFields{
"jobID": shiftResp.JobID,
}).Info("Shift: job still running, returning retryable error.")

p.controllerHelper.RecordVolumeEvent(ctx, req.Name, controllerhelpers.EventTypeNormal,
"ShiftJobRunning",
fmt.Sprintf("Shift job %s still running, will retry", shiftResp.JobID))
return nil, status.Errorf(codes.DeadlineExceeded,
"shift job %s still running for PVC %s, will retry", shiftResp.JobID, req.Name)

case shift.JobStatusFailed:
msg := fmt.Sprintf("Shift job failed for PVC %s: %s", req.Name, shiftResp.Message)
Logc(ctx).Error(msg)
p.controllerHelper.RecordVolumeEvent(ctx, req.Name, controllerhelpers.EventTypeWarning,
"ShiftJobFailed", msg)
return nil, status.Error(codes.Internal, msg)

default:
msg := fmt.Sprintf("Shift returned unknown status %q for PVC %s", shiftResp.Status, req.Name)
Logc(ctx).Error(msg)
return nil, status.Error(codes.Internal, msg)
}
}

// Check if CSI asked for a clone (overrides trident.netapp.io/cloneFromPVC PVC annotation, if present)
if req.VolumeContentSource != nil {
switch contentSource := req.VolumeContentSource.Type.(type) {
Expand Down
4 changes: 4 additions & 0 deletions frontend/csi/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
controllerAPI "github.com/netapp/trident/frontend/csi/controller_api"
controllerhelpers "github.com/netapp/trident/frontend/csi/controller_helpers"
nodehelpers "github.com/netapp/trident/frontend/csi/node_helpers"
"github.com/netapp/trident/frontend/csi/shift"
. "github.com/netapp/trident/logging"
"github.com/netapp/trident/utils/devices"
"github.com/netapp/trident/utils/errors"
Expand Down Expand Up @@ -60,6 +61,7 @@ type Plugin struct {
restClient controllerAPI.TridentController
controllerHelper controllerhelpers.ControllerHelper
nodeHelper nodehelpers.NodeHelper
shiftClient shift.Client

aesKey []byte

Expand Down Expand Up @@ -126,6 +128,7 @@ func NewControllerPlugin(
command: execCmd.NewCommand(),
osutils: osutils.New(),
activatedChan: make(chan struct{}, 1),
shiftClient: shift.NewClient(),
}

var err error
Expand Down Expand Up @@ -336,6 +339,7 @@ func NewAllInOnePlugin(
command: execCmd.NewCommand(),
osutils: osutils.New(),
activatedChan: make(chan struct{}, 1),
shiftClient: shift.NewClient(),
}

port := "34571"
Expand Down
Loading
Loading