Skip to content

Commit ca19170

Browse files
authored
create volume with transaction support (#3350)
1 parent 5bfd686 commit ca19170

File tree

15 files changed

+355
-38
lines changed

15 files changed

+355
-38
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/stretchr/testify v1.10.0
2626
github.com/vmware-tanzu/vm-operator/api v1.8.7-0.20250509154507-b93e51fc90fa
2727
github.com/vmware-tanzu/vm-operator/external/byok v0.0.0-20250509154507-b93e51fc90fa
28-
github.com/vmware/govmomi v0.51.0
28+
github.com/vmware/govmomi v0.52.0-alpha.0.0.20250624230355-39c6f0f94465
2929
go.uber.org/zap v1.27.0
3030
golang.org/x/crypto v0.36.0
3131
golang.org/x/sync v0.14.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,8 @@ github.com/vmware-tanzu/vm-operator/api v1.8.7-0.20250509154507-b93e51fc90fa h1:
293293
github.com/vmware-tanzu/vm-operator/api v1.8.7-0.20250509154507-b93e51fc90fa/go.mod h1:V0JbH4beGCU+q7yqnWUYYOuDij0ut5i1iBO4cyzg+tM=
294294
github.com/vmware-tanzu/vm-operator/external/byok v0.0.0-20250509154507-b93e51fc90fa h1:4MKu14YJ7J54O6QKmT4ds5EUpysWLLtQRMff73cVkmU=
295295
github.com/vmware-tanzu/vm-operator/external/byok v0.0.0-20250509154507-b93e51fc90fa/go.mod h1:8tiuyYslzjLIUmOlXZuGKQdQP2ZgWGCVhVeyptmZYnk=
296-
github.com/vmware/govmomi v0.51.0 h1:n3RLS9aw/irTOKbiIyJzAb6rOat4YOVv/uDoRsNTSQI=
297-
github.com/vmware/govmomi v0.51.0/go.mod h1:3ywivawGRfMP2SDCeyKqxTl2xNIHTXF0ilvp72dot5A=
296+
github.com/vmware/govmomi v0.52.0-alpha.0.0.20250624230355-39c6f0f94465 h1:BV3/IrRGRMmGWra0T4bK6xpWePsCiwLPOWns7SFREFI=
297+
github.com/vmware/govmomi v0.52.0-alpha.0.0.20250624230355-39c6f0f94465/go.mod h1:3ywivawGRfMP2SDCeyKqxTl2xNIHTXF0ilvp72dot5A=
298298
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
299299
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
300300
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=

manifests/supervisorcluster/1.29/cns-csi.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ data:
559559
"workload-domain-isolation": "false"
560560
"WCP_VMService_BYOK": "false"
561561
"file-volume-with-vm-service": "false"
562+
"csi-transaction-support": "false"
562563
kind: ConfigMap
563564
metadata:
564565
name: csi-feature-states

manifests/supervisorcluster/1.30/cns-csi.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ data:
567567
"WCP_VMService_BYOK": "false"
568568
"sv-pvc-snapshot-protection-finalizer": "false"
569569
"file-volume-with-vm-service": "false"
570+
"csi-transaction-support": "false"
570571
kind: ConfigMap
571572
metadata:
572573
name: csi-feature-states

manifests/supervisorcluster/1.31/cns-csi.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ data:
567567
"WCP_VMService_BYOK": "false"
568568
"sv-pvc-snapshot-protection-finalizer": "false"
569569
"file-volume-with-vm-service": "false"
570+
"csi-transaction-support": "false"
570571
kind: ConfigMap
571572
metadata:
572573
name: csi-feature-states

manifests/vanilla/vsphere-csi-driver.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ apiVersion: v1
150150
data:
151151
"trigger-csi-fullsync": "false"
152152
"pv-to-backingdiskobjectid-mapping": "false"
153+
"csi-transaction-support": "false"
153154
kind: ConfigMap
154155
metadata:
155156
name: internal-feature-states.csi.vsphere.vmware.com

pkg/common/cns-lib/volume/manager.go

Lines changed: 166 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ func (m *defaultManager) MonitorCreateVolumeTask(ctx context.Context,
386386
vCenterServerForVolumeOperationCR string
387387
)
388388
log := logger.GetLogger(ctx)
389+
log.Debugf("Invoked MonitorCreateVolumeTask for task: %v", task.Reference())
389390
if m.multivCenterTopologyDeployment {
390391
vCenterServerForVolumeOperationCR = m.virtualCenter.Config.Host
391392
}
@@ -467,21 +468,33 @@ func (m *defaultManager) MonitorCreateVolumeTask(ctx context.Context,
467468
}
468469
volumeOperationRes := taskResult.GetCnsVolumeOperationResult()
469470
if volumeOperationRes.Fault != nil {
470-
// Validate if the volume is already registered.
471-
faultType = ExtractFaultTypeFromVolumeResponseResult(ctx, volumeOperationRes)
472-
resp, err := validateCreateVolumeResponseFault(ctx, volNameFromInputSpec, volumeOperationRes)
471+
if IsNotSupportedFault(ctx, volumeOperationRes.Fault) {
472+
faultType = "vim25:NotSupported"
473+
err = fmt.Errorf("failed to create volume with fault: %q", faultType)
474+
} else if _, ok := volumeOperationRes.Fault.Fault.(*cnstypes.CnsVolumeAlreadyExistsFault); ok {
475+
faultType = "vim.fault.CnsVolumeAlreadyExistsFault"
476+
err = fmt.Errorf("failed to create volume with fault: %q", faultType)
477+
} else {
478+
// Validate if the volume is already registered.
479+
faultType = ExtractFaultTypeFromVolumeResponseResult(ctx, volumeOperationRes)
480+
resp, err := validateCreateVolumeResponseFault(ctx, volNameFromInputSpec, volumeOperationRes)
481+
if err == nil {
482+
*volumeOperationDetails = createRequestDetails(volNameFromInputSpec, resp.VolumeID.Id, "", 0,
483+
(*volumeOperationDetails).QuotaDetails,
484+
(*volumeOperationDetails).OperationDetails.TaskInvocationTimestamp, task.Reference().Value,
485+
vCenterServerForVolumeOperationCR, taskInfo.ActivationId, taskInvocationStatusSuccess, "")
486+
return resp, faultType, err
487+
}
488+
}
473489
if err != nil {
490+
log.Errorf("err:%v observed for task: %v, volume name: %q, OpID: %q",
491+
err.Error(), task.Reference().Value, volNameFromInputSpec, taskInfo.ActivationId)
474492
*volumeOperationDetails = createRequestDetails(volNameFromInputSpec, "", "", 0,
475493
(*volumeOperationDetails).QuotaDetails, (*volumeOperationDetails).OperationDetails.TaskInvocationTimestamp,
476494
task.Reference().Value, vCenterServerForVolumeOperationCR, taskInfo.ActivationId,
477495
taskInvocationStatusError, err.Error())
478-
} else {
479-
*volumeOperationDetails = createRequestDetails(volNameFromInputSpec, resp.VolumeID.Id, "", 0,
480-
(*volumeOperationDetails).QuotaDetails,
481-
(*volumeOperationDetails).OperationDetails.TaskInvocationTimestamp, task.Reference().Value,
482-
vCenterServerForVolumeOperationCR, taskInfo.ActivationId, taskInvocationStatusSuccess, "")
496+
return nil, faultType, err
483497
}
484-
return resp, faultType, err
485498
}
486499
// Extract the CnsVolumeInfo from the taskResult.
487500
resp, faultType, err := getCnsVolumeInfoFromTaskResult(ctx, m.virtualCenter, volNameFromInputSpec,
@@ -658,6 +671,125 @@ func (m *defaultManager) createVolumeWithImprovedIdempotency(ctx context.Context
658671
spec.Metadata.ContainerClusterArray[0].ClusterId)
659672
}
660673

674+
// createVolumeWithTransaction creates volume with supplied PVC UUID as VolumeID in the CreateVolumeSpec
675+
func (m *defaultManager) createVolumeWithTransaction(ctx context.Context, spec *cnstypes.CnsVolumeCreateSpec,
676+
extraParams interface{}) (resp *CnsVolumeInfo, faultType string, finalErr error) {
677+
log := logger.GetLogger(ctx)
678+
var (
679+
// Store the volume name passed in by input spec, this
680+
// name may exceed 80 characters.
681+
volNameFromInputSpec = spec.Name
682+
// Reference to the CreateVolume task on CNS.
683+
task *object.Task
684+
// Local instance of CreateVolume details that needs to
685+
// be persisted.
686+
volumeOperationDetails *cnsvolumeoperationrequest.VolumeOperationRequestDetails
687+
// quotaInfo consists of values required to populate QuotaDetails in CnsVolumeOperationRequest CR.
688+
quotaInfo *cnsvolumeoperationrequest.QuotaDetails
689+
isPodVMOnStretchSupervisorFSSEnabled bool
690+
)
691+
692+
if extraParams != nil {
693+
createVolParams, ok := extraParams.(*CreateVolumeExtraParams)
694+
if !ok {
695+
return nil, csifault.CSIInternalFault,
696+
logger.LogNewErrorf(log, "unrecognised type for CreateVolume params: %+v", extraParams)
697+
}
698+
log.Debugf("Received CreateVolume extraParams: %+v", *createVolParams)
699+
700+
isPodVMOnStretchSupervisorFSSEnabled = createVolParams.IsPodVMOnStretchSupervisorFSSEnabled
701+
if isPodVMOnStretchSupervisorFSSEnabled && m.clusterFlavor == cnstypes.CnsClusterFlavorWorkload {
702+
var storagePolicyID string
703+
if len(spec.Profile) >= 1 {
704+
storagePolicyID = spec.Profile[0].(*vim25types.VirtualMachineDefinedProfileSpec).ProfileId
705+
}
706+
reservedQty := resource.NewQuantity(createVolParams.VolSizeBytes, resource.BinarySI)
707+
quotaInfo = &cnsvolumeoperationrequest.QuotaDetails{
708+
Reserved: reservedQty,
709+
StoragePolicyId: storagePolicyID,
710+
StorageClassName: createVolParams.StorageClassName,
711+
Namespace: createVolParams.Namespace,
712+
}
713+
log.Infof("QuotaInfo during CreateVolume call: %+v", *quotaInfo)
714+
}
715+
}
716+
717+
if m.operationStore == nil {
718+
return nil, csifault.CSIInternalFault, logger.LogNewError(log, "operation store cannot be nil")
719+
}
720+
var vCenterServerForVolumeOperationCR string
721+
if m.multivCenterTopologyDeployment {
722+
vCenterServerForVolumeOperationCR = m.virtualCenter.Config.Host
723+
}
724+
725+
volumeOperationDetails, finalErr = m.operationStore.GetRequestDetails(ctx, volNameFromInputSpec)
726+
if !apierrors.IsNotFound(finalErr) {
727+
return nil, csifault.CSIInternalFault, finalErr
728+
}
729+
defer func() {
730+
// Persist the operation details before returning. Only success or error
731+
// needs to be stored as InProgress details are stored when the task is
732+
// created on CNS.
733+
if volumeOperationDetails != nil && volumeOperationDetails.OperationDetails != nil &&
734+
volumeOperationDetails.OperationDetails.TaskStatus != taskInvocationStatusInProgress {
735+
736+
if m.clusterFlavor == cnstypes.CnsClusterFlavorWorkload && isPodVMOnStretchSupervisorFSSEnabled {
737+
// Decrease the reserved field in QuotaDetails when the CreateVolume task is
738+
// successful or has errored out.
739+
taskStatus := volumeOperationDetails.OperationDetails.TaskStatus
740+
if (taskStatus == taskInvocationStatusSuccess || taskStatus == taskInvocationStatusError) &&
741+
volumeOperationDetails.QuotaDetails != nil {
742+
volumeOperationDetails.QuotaDetails.Reserved = resource.NewQuantity(0,
743+
resource.BinarySI)
744+
log.Infof("Setting the reserved field for VolumeOperationDetails instance %s to 0",
745+
volumeOperationDetails.Name)
746+
}
747+
tempErr := m.operationStore.StoreRequestDetails(ctx, volumeOperationDetails)
748+
if finalErr == nil && tempErr != nil {
749+
log.Errorf("failed to store CreateVolume details with error: %v", tempErr)
750+
finalErr = tempErr
751+
}
752+
} else {
753+
err := m.operationStore.StoreRequestDetails(ctx, volumeOperationDetails)
754+
if err != nil {
755+
log.Warnf("failed to store CreateVolume details with error: %v", err)
756+
}
757+
}
758+
}
759+
}()
760+
volumeOperationDetails = createRequestDetails(volNameFromInputSpec, "", "", 0,
761+
quotaInfo, metav1.Now(), "", vCenterServerForVolumeOperationCR, "",
762+
taskInvocationStatusInProgress, "")
763+
err := m.operationStore.StoreRequestDetails(ctx, volumeOperationDetails)
764+
if err != nil {
765+
// Don't return if CreateVolume details can't be stored.
766+
log.Warnf("failed to store CreateVolume details with error: %v", err)
767+
}
768+
task, finalErr = invokeCNSCreateVolume(ctx, m.virtualCenter, spec)
769+
if finalErr != nil {
770+
log.Errorf("failed to create volume with error: %v", finalErr)
771+
volumeOperationDetails = createRequestDetails(volNameFromInputSpec, "", "", 0,
772+
quotaInfo, volumeOperationDetails.OperationDetails.TaskInvocationTimestamp, "",
773+
vCenterServerForVolumeOperationCR, "", taskInvocationStatusError, finalErr.Error())
774+
faultType = ExtractFaultTypeFromErr(ctx, finalErr)
775+
return nil, faultType, finalErr
776+
}
777+
if !isStaticallyProvisioned(spec) {
778+
// Persist task details only for dynamically provisioned volumes.
779+
volumeOperationDetails = createRequestDetails(volNameFromInputSpec, "", "", 0,
780+
quotaInfo, metav1.Now(), task.Reference().Value, vCenterServerForVolumeOperationCR, "",
781+
taskInvocationStatusInProgress, "")
782+
err := m.operationStore.StoreRequestDetails(ctx, volumeOperationDetails)
783+
if err != nil {
784+
// Don't return if CreateVolume details can't be stored.
785+
log.Warnf("failed to store CreateVolume details with error: %v", err)
786+
}
787+
}
788+
789+
return m.MonitorCreateVolumeTask(ctx, &volumeOperationDetails, task, volNameFromInputSpec,
790+
spec.Metadata.ContainerClusterArray[0].ClusterId)
791+
}
792+
661793
// IsTaskPending returns true in two cases -
662794
// 1. if the task status was in progress
663795
// 2. if the status was an error but the error was for adding the task to the listview
@@ -853,7 +985,29 @@ func (m *defaultManager) CreateVolume(ctx context.Context, spec *cnstypes.CnsVol
853985
}
854986
// Call CreateVolume implementation based on FSS value.
855987
if m.idempotencyHandlingEnabled {
856-
return m.createVolumeWithImprovedIdempotency(ctx, spec, extraParams)
988+
if spec.VolumeId != nil && spec.VolumeId.Id != "" {
989+
var cnsVolumeInfo *CnsVolumeInfo
990+
cnsVolumeInfo, faultType, err = m.createVolumeWithTransaction(ctx, spec, extraParams)
991+
if err != nil {
992+
log.Errorf("failed to create volume with VolumeID: %q, faultType: %q, err: %v",
993+
spec.VolumeId.Id, faultType, err)
994+
if IsCnsVolumeAlreadyExistsFault(ctx, faultType) {
995+
log.Infof("Observed volume with Id: %q is already Exists. Deleting Volume.", spec.VolumeId.Id)
996+
deleteFaultType, deleteError := m.deleteVolume(ctx, spec.VolumeId.Id, true)
997+
if deleteError != nil {
998+
log.Errorf("failed to delete volume: %q to handle CnsVolumeAlreadyExistsFault , err :%v", spec.VolumeId.Id, err)
999+
return nil, deleteFaultType, deleteError
1000+
}
1001+
log.Infof("Attempt to re-create volume with Id: %q", spec.VolumeId.Id)
1002+
cnsVolumeInfo, faultType, err = m.createVolumeWithTransaction(ctx, spec, extraParams)
1003+
return cnsVolumeInfo, faultType, err
1004+
}
1005+
return nil, faultType, err
1006+
}
1007+
return cnsVolumeInfo, faultType, err
1008+
} else {
1009+
return m.createVolumeWithImprovedIdempotency(ctx, spec, extraParams)
1010+
}
8571011
}
8581012
return m.createVolume(ctx, spec)
8591013
}
@@ -1854,7 +2008,7 @@ func (m *defaultManager) expandVolumeWithImprovedIdempotency(ctx context.Context
18542008

18552009
volumeOperationRes := taskResult.GetCnsVolumeOperationResult()
18562010
if volumeOperationRes.Fault != nil {
1857-
if _, ok := volumeOperationRes.Fault.Fault.(cnstypes.CnsFault); ok {
2011+
if _, ok := volumeOperationRes.Fault.Fault.(*cnstypes.CnsFault); ok {
18582012
log.Debugf("ExtendVolume task %s returned with CnsFault. Querying CNS to "+
18592013
"determine if volume with ID %s was successfully expanded.",
18602014
task.Reference().Value, volumeID)
@@ -2562,7 +2716,7 @@ func (m *defaultManager) createSnapshotWithImprovedIdempotencyCheck(ctx context.
25622716
errMsg = fmt.Sprintf("snapshot %q on volume %q got created, but post-processing failed. "+
25632717
"Fault: %q, opID: %q", instanceName, volumeID, spew.Sdump(createSnapshotsOperationRes.Fault),
25642718
createSnapshotsTaskInfo.ActivationId)
2565-
snapshotId := createSnapshotsOperationRes.Fault.Fault.(cnstypes.CnsSnapshotCreatedFault).SnapshotId.Id
2719+
snapshotId := createSnapshotsOperationRes.Fault.Fault.(*cnstypes.CnsSnapshotCreatedFault).SnapshotId.Id
25662720
volumeOperationDetails = createRequestDetails(instanceName, volumeID, snapshotId, 0, nil,
25672721
volumeOperationDetails.OperationDetails.TaskInvocationTimestamp, createSnapshotsTask.Reference().Value,
25682722
"", createSnapshotsTaskInfo.ActivationId, taskInvocationStatusPartiallyFailed, errMsg)

pkg/common/cns-lib/volume/util.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ func getTaskResultFromTaskInfo(ctx context.Context, taskInfo *types.TaskInfo) (c
284284
func validateCreateVolumeResponseFault(ctx context.Context, name string,
285285
resp *cnstypes.CnsVolumeOperationResult) (*CnsVolumeInfo, error) {
286286
log := logger.GetLogger(ctx)
287-
fault, ok := resp.Fault.Fault.(cnstypes.CnsAlreadyRegisteredFault)
287+
fault, ok := resp.Fault.Fault.(*cnstypes.CnsAlreadyRegisteredFault)
288288
if ok {
289289
log.Infof("Volume is already registered with CNS. VolumeName: %q, volumeID: %q",
290290
name, fault.VolumeId.Id)
@@ -374,11 +374,13 @@ func ExtractFaultTypeFromVolumeResponseResult(ctx context.Context,
374374
faultType, fault.Fault, fault, resp)
375375
slice := strings.Split(faultType, ".")
376376
vimFaultType := csifault.VimFaultPrefix + slice[1]
377+
log.Infof("returning fault: %q", vimFaultType)
377378
return vimFaultType
378379
} else {
379380
faultType = reflect.TypeOf(fault).String()
380381
log.Infof("Extract fault: %q from resp: %+v",
381382
faultType, resp)
383+
log.Infof("returning fault: %q", faultType)
382384
return faultType
383385
}
384386
}
@@ -557,3 +559,37 @@ func IsNotFoundFault(ctx context.Context, faultType string) bool {
557559
return faultType == "vim.fault.NotFound"
558560

559561
}
562+
563+
// IsNotSupportedFault returns true if a given fault is NotSupported fault
564+
func IsNotSupportedFault(ctx context.Context, fault *types.LocalizedMethodFault) bool {
565+
log := logger.GetLogger(ctx)
566+
if cnsFault, ok := fault.Fault.(*cnstypes.CnsFault); ok {
567+
if cause := cnsFault.FaultCause; cause != nil {
568+
if innerfault, ok := cause.Fault.(*types.NotSupported); ok {
569+
log.Info("observed NotSupported fault")
570+
return true
571+
} else {
572+
log.Infof("observed fault: %T", innerfault)
573+
return false
574+
}
575+
} else {
576+
log.Errorf("observed fault with nil cause")
577+
}
578+
} else {
579+
log.Errorf("can not typecast fault to CnsFault")
580+
}
581+
return false
582+
}
583+
584+
func IsNotSupportedFaultType(ctx context.Context, faultType string) bool {
585+
log := logger.GetLogger(ctx)
586+
log.Infof("Checking fault type: %q is vim25:NotSupported", faultType)
587+
return faultType == "vim25:NotSupported"
588+
}
589+
590+
// IsCnsVolumeAlreadyExistsFault returns true if a given faultType value is vim.fault.CnsVolumeAlreadyExistsFault
591+
func IsCnsVolumeAlreadyExistsFault(ctx context.Context, faultType string) bool {
592+
log := logger.GetLogger(ctx)
593+
log.Infof("Checking fault type: %q is vim.fault.CnsVolumeAlreadyExistsFault", faultType)
594+
return faultType == "vim.fault.CnsVolumeAlreadyExistsFault"
595+
}

pkg/common/cns-lib/vsphere/utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func IsVimFaultNotFoundError(err error) bool {
9797
func IsCnsSnapshotCreatedFaultError(err error) bool {
9898
isCnsSnapshotCreatedFaultError := false
9999
if soap.IsVimFault(err) {
100-
_, isCnsSnapshotCreatedFaultError = soap.ToVimFault(err).(cnstypes.CnsSnapshotCreatedFault)
100+
_, isCnsSnapshotCreatedFaultError = soap.ToVimFault(err).(*cnstypes.CnsSnapshotCreatedFault)
101101
}
102102
return isCnsSnapshotCreatedFaultError
103103
}
@@ -106,7 +106,7 @@ func IsCnsSnapshotCreatedFaultError(err error) bool {
106106
func IsCnsSnapshotNotFoundError(err error) bool {
107107
isCnsSnapshotNotFoundError := false
108108
if soap.IsVimFault(err) {
109-
_, isCnsSnapshotNotFoundError = soap.ToVimFault(err).(cnstypes.CnsSnapshotNotFoundFault)
109+
_, isCnsSnapshotNotFoundError = soap.ToVimFault(err).(*cnstypes.CnsSnapshotNotFoundFault)
110110
}
111111
return isCnsSnapshotNotFoundError
112112
}

pkg/csi/service/common/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ const (
440440
FileVolumesWithVmService = "file-volume-with-vm-service"
441441
// SharedDiskFss is an FSS that tells whether shared disks are supported or not
442442
SharedDiskFss = "supports_shared_disks"
443+
// CSITranSactionSupport is an FSS for transaction support
444+
CSITranSactionSupport = "csi-transaction-support"
443445
)
444446

445447
var WCPFeatureStates = map[string]struct{}{

0 commit comments

Comments
 (0)