@@ -24,6 +24,7 @@ import (
2424 "time"
2525
2626 cnstypes "github.com/vmware/govmomi/cns/types"
27+ vimtypes "github.com/vmware/govmomi/vim25/types"
2728 v1 "k8s.io/api/core/v1"
2829 "k8s.io/apimachinery/pkg/api/equality"
2930 apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -50,6 +51,7 @@ import (
5051 infraspiv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/infrastoragepolicyinfo/v1alpha1"
5152 storagepolicyv1alpha2 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/storagepolicy/v1alpha2"
5253 spiv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/storagepolicyinfo/v1alpha1"
54+ cnsvsphere "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/cns-lib/vsphere"
5355 volumes "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/cns-lib/volume"
5456 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/config"
5557 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/common"
@@ -58,6 +60,7 @@ import (
5860 k8s "sigs.k8s.io/vsphere-csi-driver/v3/pkg/kubernetes"
5961 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/syncer/cnsoperator/types"
6062 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/syncer/cnsoperator/util"
63+ "sigs.k8s.io/vsphere-csi-driver/v3/pkg/syncer/cnsoperator/vsphereinfra"
6164)
6265
6366// zonesProvider is the narrow slice of COCommonInterface that this controller
@@ -489,6 +492,18 @@ func (r *ReconcileStoragePolicyInfo) Reconcile(ctx context.Context,
489492 return r .completeReconciliationWithError (ctx , request .NamespacedName , timeout , err )
490493 }
491494
495+ // Populate VolumeCapabilities from InfraStoragePolicyInfo.
496+ if err := syncVolumeCapabilitiesFromInfraSPI (ctx , instance , infraSPI ); err != nil {
497+ log .Errorf ("Failed to sync volume capabilities for StoragePolicyInfo %q: %v" ,
498+ request .NamespacedName , err )
499+ if setErr := r .setSPIError (ctx , instance ,
500+ fmt .Sprintf ("Failed to sync volume capabilities: %v" , err )); setErr != nil {
501+ log .Errorf ("Failed to update StoragePolicyInfo %q status: %v" ,
502+ request .NamespacedName , setErr )
503+ }
504+ return r .completeReconciliationWithError (ctx , request .NamespacedName , timeout , err )
505+ }
506+
492507 if setErr := r .setSPISuccess (ctx , instance , "Successfully synced topology" ); setErr != nil {
493508 log .Errorf ("Failed to update StoragePolicyInfo %q status: %v" ,
494509 request .NamespacedName , setErr )
@@ -623,6 +638,165 @@ func (r *ReconcileStoragePolicyInfo) syncTopologyFromInfraSPI(ctx context.Contex
623638 return nil
624639}
625640
641+ // syncVolumeCapabilitiesFromInfraSPI populates the namespace-scoped
642+ // StoragePolicyInfo volume capabilities from the cluster-scoped
643+ // InfraStoragePolicyInfo, with no additional vCenter calls.
644+ // SupportsVolumeModeFilesystem is always true, independent of InfraSPI.
645+ // SupportsVolumeModeBlock is cluster-wide (not namespace-filtered like topology), so
646+ // it is copied as-is from InfraSPI. SupportsLinkedClone and
647+ // SupportsHighPerformanceLinkedClone are recomputed for just the zones accessible to
648+ // this namespace; see linkedCloneCapabilitiesForNamespace.
649+ func syncVolumeCapabilitiesFromInfraSPI (ctx context.Context , instance * spiv1alpha1.StoragePolicyInfo ,
650+ infraSPI * infraspiv1alpha1.InfraStoragePolicyInfo ) error {
651+ infraCaps := infraSPI .Status .VolumeCapabilities
652+
653+ lc , hplc , err := linkedCloneCapabilitiesForNamespace (ctx , instance , infraSPI , infraCaps )
654+ if err != nil {
655+ return err
656+ }
657+
658+ instance .Status .VolumeCapabilities = map [spiv1alpha1.VolumeCapability ]bool {
659+ spiv1alpha1 .SupportsVolumeModeFilesystem : true ,
660+ spiv1alpha1 .SupportsVolumeModeBlock : infraCaps [infraspiv1alpha1 .SupportsVolumeModeBlock ],
661+ spiv1alpha1 .SupportsLinkedClone : lc ,
662+ spiv1alpha1 .SupportsHighPerformanceLinkedClone : hplc ,
663+ }
664+ return nil
665+ }
666+
667+ // linkedCloneCapabilitiesForNamespace determines SupportsLinkedClone and
668+ // SupportsHighPerformanceLinkedClone for this namespace's StoragePolicyInfo. When the
669+ // policy has no topology (non-zonal), there is no per-namespace zone subset to check
670+ // against, so the InfraSPI-computed cluster-wide results are used directly. Otherwise
671+ // both are recomputed over just the namespace's accessible zones (already
672+ // namespace-filtered by syncTopologyFromInfraSPI) via computeLinkedCloneForNamespace /
673+ // computeHPLCForNamespace.
674+ func linkedCloneCapabilitiesForNamespace (ctx context.Context , instance * spiv1alpha1.StoragePolicyInfo ,
675+ infraSPI * infraspiv1alpha1.InfraStoragePolicyInfo ,
676+ infraCaps map [infraspiv1alpha1.VolumeCapability ]bool ) (lc bool , hplc bool , err error ) {
677+ if instance .Status .TopologyInfo == nil {
678+ return infraCaps [infraspiv1alpha1 .SupportsLinkedClone ],
679+ infraCaps [infraspiv1alpha1 .SupportsHighPerformanceLinkedClone ], nil
680+ }
681+
682+ nsZones := instance .Status .TopologyInfo .AccessibleZones
683+ lc , err = computeLinkedCloneForNamespace (ctx , infraSPI .Name , nsZones )
684+ if err != nil {
685+ return false , false , err
686+ }
687+ hplc , err = computeHPLCForNamespace (ctx , infraSPI .Name , nsZones , lc )
688+ if err != nil {
689+ return false , false , err
690+ }
691+ return lc , hplc , nil
692+ }
693+
694+ // computeLinkedCloneForNamespace determines whether the storage policy identified by
695+ // policyName (its K8s-compliant name) supports LinkedClone within nsZones, using only
696+ // data already cached by vsphereinfra.StoragePolicyInfoCache — no additional vCenter
697+ // calls are made. LinkedClone is supported for the namespace if any host mounting a
698+ // policy-compatible datastore within one of nsZones is running ESXi 9.1 or above.
699+ // See anyQualifyingHostForNamespace for the cache fields this relies on.
700+ func computeLinkedCloneForNamespace (ctx context.Context , policyName string , nsZones []string ) (bool , error ) {
701+ return anyQualifyingHostForNamespace (ctx , policyName , nsZones , hostSupportsLinkedClone )
702+ }
703+
704+ // computeHPLCForNamespace determines whether SupportsHighPerformanceLinkedClone is true
705+ // for nsZones. HPLC requires LinkedClone support first — if lcSupported is false, HPLC is
706+ // false without evaluating the vSAN-ESA condition. Otherwise it looks for a qualifying
707+ // host (ESXi 9.1+) whose cluster has vSAN-ESA enabled
708+ // (hostSupportsHighPerformanceLinkedClone), using only cached data — no additional
709+ // vCenter calls.
710+ func computeHPLCForNamespace (ctx context.Context , policyName string , nsZones []string ,
711+ lcSupported bool ) (bool , error ) {
712+ if ! lcSupported {
713+ return false , nil
714+ }
715+ return anyQualifyingHostForNamespace (ctx , policyName , nsZones , hostSupportsHighPerformanceLinkedClone )
716+ }
717+
718+ // anyQualifyingHostForNamespace walks every host mounting a datastore that is both
719+ // compatible with policyName and within one of nsZones, returning true as soon as
720+ // qualifies reports true for one of them. No additional vCenter calls are made:
721+ //
722+ // - policy → compatible datastores: DsToPolicy, populated by
723+ // util.GetPolicyCompatibleDatastoresPerZone/GetAccessibleZonesAndDatastoresForPolicy
724+ // while the clusterstoragepolicyinfo controller reconciles InfraSPI.
725+ // - zone → datastores: ZoneToDatastores, populated as a byproduct of the same call,
726+ // independent of any storage policy.
727+ // - datastore → hosts: DsToHosts, kept live by the PropertyCollector inventory watcher.
728+ func anyQualifyingHostForNamespace (ctx context.Context , policyName string , nsZones []string ,
729+ qualifies func (ctx context.Context , hostID string ) (bool , error )) (bool , error ) {
730+ compatibleDS := vsphereinfra .GetCache ().GetDatastoresForPolicy (policyName )
731+ if len (compatibleDS ) == 0 {
732+ return false , nil
733+ }
734+ compatibleDSSet := make (map [string ]struct {}, len (compatibleDS ))
735+ for _ , dsID := range compatibleDS {
736+ compatibleDSSet [dsID ] = struct {}{}
737+ }
738+
739+ for _ , zone := range nsZones {
740+ zoneDS , ok := vsphereinfra .GetCache ().GetZoneDatastores (zone )
741+ if ! ok {
742+ continue
743+ }
744+ for dsID := range zoneDS {
745+ if _ , compatible := compatibleDSSet [dsID ]; ! compatible {
746+ continue
747+ }
748+ hosts , ok := vsphereinfra .GetCache ().GetDsHosts (dsID )
749+ if ! ok {
750+ continue
751+ }
752+ for hostID := range hosts {
753+ qualified , err := qualifies (ctx , hostID )
754+ if err != nil {
755+ return false , err
756+ }
757+ if qualified {
758+ return true , nil
759+ }
760+ }
761+ }
762+ }
763+ return false , nil
764+ }
765+
766+ // hostSupportsLinkedClone reports whether a host cached by the inventory watcher is
767+ // running ESXi 9.1 or above. Returns false if the host's version has not been observed
768+ // yet; returns an error if the cached version string can't be parsed.
769+ func hostSupportsLinkedClone (ctx context.Context , hostID string ) (bool , error ) {
770+ version , found := vsphereinfra .GetCache ().GetHostVersion (hostID )
771+ if ! found {
772+ return false , nil
773+ }
774+ supported , err := cnsvsphere .IsvSphereVersion91orAbove (ctx , vimtypes.AboutInfo {Version : version })
775+ if err != nil {
776+ return false , fmt .Errorf ("failed to parse ESXi version %q for host %q: %w" , version , hostID , err )
777+ }
778+ return supported , nil
779+ }
780+
781+ // hostSupportsHighPerformanceLinkedClone reports whether a host cached by the inventory
782+ // watcher both is ESXi 9.1+ and belongs to a cluster with vSAN-ESA enabled
783+ // (ClusterForHost/GetClusterESAEnabled, populated by the clusterstoragepolicyinfo
784+ // controller's isClusterESAEnabled check while reconciling InfraSPI). Returns false if
785+ // the host's cluster, or that cluster's vSAN-ESA state, has not been observed/computed
786+ // yet.
787+ func hostSupportsHighPerformanceLinkedClone (ctx context.Context , hostID string ) (bool , error ) {
788+ lc , err := hostSupportsLinkedClone (ctx , hostID )
789+ if err != nil || ! lc {
790+ return false , err
791+ }
792+ clusterID , ok := vsphereinfra .GetCache ().ClusterForHost (hostID )
793+ if ! ok {
794+ return false , nil
795+ }
796+ esa , found := vsphereinfra .GetCache ().GetClusterESAEnabled (clusterID )
797+ return found && esa , nil
798+ }
799+
626800// syncMarkerPolicyTopology populates the namespace-scoped StoragePolicyInfo
627801// topology for a marker policy by deriving accessible zones from FVS instance
628802// namespaces that share the consumer namespace's VPC path, bypassing the
0 commit comments