Skip to content
Draft
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
2 changes: 1 addition & 1 deletion deploy/bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ spec:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: trident-operator
image: docker.io/netapp/trident-operator:26.06.0
image: quay.io/pradeepkumar27x/trident-operator:26.06.0-custom-linux-amd64
imagePullPolicy: IfNotPresent
name: trident-operator
resources:
Expand Down
2 changes: 1 addition & 1 deletion deploy/crds/tridentorchestrator_cr_imagepullsecrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ metadata:
spec:
debug: true
namespace: trident
tridentImage: netapp/trident:26.06.0
tridentImage: quay.io/pradeepkumar27x/trident:26.06.0-custom-linux-amd64
imagePullSecrets:
- thisisasecret
2 changes: 1 addition & 1 deletion deploy/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ spec:
serviceAccountName: trident-operator
containers:
- name: trident-operator
image: docker.io/netapp/trident-operator:26.06.0
image: quay.io/pradeepkumar27x/trident-operator:26.06.0-custom-linux-amd64
command:
- "/trident-operator"
- "--debug"
Expand Down
11 changes: 11 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,17 @@ 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"

// 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
207 changes: 207 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 All @@ -15,6 +16,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"

"github.com/netapp/trident/config"
frontendcommon "github.com/netapp/trident/frontend/common"
Expand Down Expand Up @@ -99,6 +101,31 @@ 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{
"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 @@ -753,6 +780,42 @@ func (h *helper) RecordVolumeEvent(ctx context.Context, name, eventType, reason,
}
}

// PatchVolumeAnnotations merges the supplied annotations into the PVC identified by the
// given CSI volume name (pvc-<uid>). Existing annotations are preserved; only the supplied
// keys are added or overwritten.
func (h *helper) PatchVolumeAnnotations(
ctx context.Context, name string, annotations map[string]string,
) error {
pvc, err := h.getPVCForCSIVolume(ctx, name)
if err != nil {
return fmt.Errorf("failed to find PVC for volume %s: %v", name, err)
}

patch := map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": annotations,
},
}
patchBytes, err := json.Marshal(patch)
if err != nil {
return fmt.Errorf("failed to marshal annotation patch: %v", err)
}

_, err = h.kubeClient.CoreV1().PersistentVolumeClaims(pvc.Namespace).Patch(
ctx, pvc.Name, k8stypes.MergePatchType, patchBytes, metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("failed to patch PVC %s/%s annotations: %v", pvc.Namespace, pvc.Name, err)
}

Logc(ctx).WithFields(LogFields{
"pvc": pvc.Name,
"namespace": pvc.Namespace,
"annotations": annotations,
}).Info("Patched PVC annotations successfully.")

return nil
}

// RecordNodeEvent accepts the name of a CSI volume (i.e. a PV name), finds the associated
// PVC, and posts and event message on the PVC object with the K8S API server.
func (h *helper) RecordNodeEvent(ctx context.Context, name, eventType, reason, message string) {
Expand Down Expand Up @@ -965,6 +1028,150 @@ 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) {
backendRef := scAnnotations[AnnShiftTridentBackendUUID]
if backendRef == "" {
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 (accepts name or UUID)
mgmtLIF, svm, username, password, err := h.resolveOntapCredentials(ctx, backendRef)
if err != nil {
return nil, fmt.Errorf("failed to resolve ONTAP credentials for backend %s: %v", backendRef, err)
}

return &storage.ShiftConfig{
BackendUUID: backendRef,
ManagementLIF: mgmtLIF,
SVM: svm,
Username: username,
Password: password,
DiskPath: diskPath,
NFSServer: nfsServer,
NFSPath: nfsPath,
VMID: pvcAnn[AnnMTVVMID],
VMUUID: pvcAnn[AnnMTVVMUUID],
PVCUID: string(pvc.UID),
}, nil
}

// resolveOntapCredentials fetches the ManagementLIF and SVM from the BackendExternal,
// then reads the ONTAP username/password from the Kubernetes Secret referenced by the TBC.
// The backendRef may be a backend name or a backend UUID; both are tried.
func (h *helper) resolveOntapCredentials(
ctx context.Context, backendRef string,
) (mgmtLIF, svm, username, password string, err error) {

Logc(ctx).WithField("backendRef", backendRef).Debug("Shift: looking up backend by name first, then by UUID.")

backendExt, err := h.orchestrator.GetBackend(ctx, backendRef)
if err != nil {
Logc(ctx).WithField("backendRef", backendRef).Debug("Shift: GetBackend by name failed, trying GetBackendByBackendUUID.")
backendExt, err = h.orchestrator.GetBackendByBackendUUID(ctx, backendRef)
if err != nil {
return "", "", "", "", fmt.Errorf("backend %s not found by name or UUID: %v", backendRef, 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.")

// The configRef on the backend is the TBC's Kubernetes UID, not its name.
// List all TBCs and find the one whose UID matches.
configRef := backendExt.ConfigRef
if configRef == "" {
return "", "", "", "", fmt.Errorf("backend %s has no configRef (TBC)", backendRef)
}

tbcList, tbcErr := h.tridentClient.TridentV1().TridentBackendConfigs(h.namespace).List(ctx, listOpts)
if tbcErr != nil {
return "", "", "", "", fmt.Errorf("failed to list TBCs: %v", tbcErr)
}

var secretName string
for _, tbc := range tbcList.Items {
if string(tbc.UID) == configRef {
Logc(ctx).WithFields(LogFields{
"tbcName": tbc.Name,
"tbcUID": tbc.UID,
"configRef": configRef,
}).Debug("Shift: found TBC matching backend configRef.")

sName, sErr := tbc.Spec.GetSecretName()
if sErr != nil {
return "", "", "", "", fmt.Errorf("failed to get secret name from TBC %s: %v", tbc.Name, sErr)
}
secretName = sName
break
}
}

if secretName == "" {
return "", "", "", "", fmt.Errorf("no TBC found with UID %s, or TBC has no credentials secret", 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
5 changes: 5 additions & 0 deletions frontend/csi/controller_helpers/plain/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ func (h *helper) RecordVolumeEvent(ctx context.Context, name, eventType, reason,
}).Trace("Volume event.")
}

// PatchVolumeAnnotations is a no-op in plain CSI mode (no Kubernetes API).
func (h *helper) PatchVolumeAnnotations(_ context.Context, _ string, _ map[string]string) error {
return nil
}

// RecordNodeEvent accepts the name of a CSI node and writes the specified
// event message to the debug Log().
func (h *helper) RecordNodeEvent(ctx context.Context, name, eventType, reason, message string) {
Expand Down
5 changes: 5 additions & 0 deletions frontend/csi/controller_helpers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ type ControllerHelper interface {
// event message in a manner appropriate to the container orchestrator.
RecordVolumeEvent(ctx context.Context, name, eventType, reason, message string)

// PatchVolumeAnnotations merges the supplied annotations into the PVC identified
// by the given CSI volume name (pvc-<uid>). Existing annotations are preserved;
// only the supplied keys are added or overwritten.
PatchVolumeAnnotations(ctx context.Context, name string, annotations map[string]string) error

// RecordNodeEvent accepts the name of a CSI node and writes the specified
// event message in a manner appropriate to the container orchestrator.
RecordNodeEvent(ctx context.Context, name, eventType, reason, message string)
Expand Down
Loading