Skip to content

Commit e586ce1

Browse files
GCNV NAS: hyphen-to-underscore normalization for volume IDs
1 parent ccc9d84 commit e586ce1

File tree

8 files changed

+634
-23
lines changed

8 files changed

+634
-23
lines changed

storage_drivers/gcp/api/gcnv.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,12 @@ func (c Client) newVolumeFromGCNVVolume(ctx context.Context, volume *netapppb.Vo
479479
}
480480
}
481481

482+
cPool := c.capacityPool(volume.StoragePool)
483+
storagePoolType := ""
484+
if cPool != nil {
485+
storagePoolType = cPool.PoolType
486+
}
487+
482488
result := &Volume{
483489
Name: volumeName,
484490
CreationToken: volume.ShareName,
@@ -488,7 +494,8 @@ func (c Client) newVolumeFromGCNVVolume(ctx context.Context, volume *netapppb.Vo
488494
CapacityPool: volume.StoragePool,
489495
NetworkName: network,
490496
NetworkFullName: volume.Network,
491-
ServiceLevel: ServiceLevelFromCapacityPool(c.capacityPool(volume.StoragePool)),
497+
ServiceLevel: ServiceLevelFromCapacityPool(cPool),
498+
StoragePoolType: storagePoolType,
492499
SizeBytes: volume.CapacityGib * GiBBytes,
493500
ExportPolicy: c.exportPolicyImport(volume.ExportPolicy),
494501
ProtocolTypes: protocolTypes,
@@ -651,14 +658,38 @@ func (c Client) Volumes(ctx context.Context) (*[]*Volume, error) {
651658
}
652659

653660
// Volume uses a volume config record to fetch a volume by the most efficient means.
661+
func gcnvVolumeFallbackNames(volConfig *storage.VolumeConfig) (string, string) {
662+
primary := volConfig.InternalName
663+
secondary := strings.ReplaceAll(volConfig.Name, "-", "_")
664+
if secondary == primary {
665+
secondary = ""
666+
}
667+
return primary, secondary
668+
}
669+
654670
func (c Client) Volume(ctx context.Context, volConfig *storage.VolumeConfig) (*Volume, error) {
655671
// When we know the internal ID, use that as it is vastly more efficient
656672
if volConfig.InternalID != "" {
657673
return c.VolumeByID(ctx, volConfig.InternalID)
658674
}
659675

660-
// Fall back to the name
661-
return c.VolumeByName(ctx, volConfig.InternalName)
676+
primaryName, secondaryName := gcnvVolumeFallbackNames(volConfig)
677+
678+
// Fall back to the creation token.
679+
volume, err := c.VolumeByName(ctx, primaryName)
680+
if err == nil {
681+
return volume, nil
682+
}
683+
if !errors.IsNotFoundError(err) {
684+
return nil, err
685+
}
686+
687+
// For UNIFIED naming, also try underscore-normalized display name.
688+
if secondaryName == "" {
689+
return nil, err
690+
}
691+
692+
return c.VolumeByName(ctx, secondaryName)
662693
}
663694

664695
func (c Client) VolumeByName(ctx context.Context, name string) (*Volume, error) {
@@ -734,8 +765,23 @@ func (c Client) VolumeExists(ctx context.Context, volConfig *storage.VolumeConfi
734765
if volConfig.InternalID != "" {
735766
return c.VolumeExistsByID(ctx, volConfig.InternalID)
736767
}
737-
// Fall back to the creation token
738-
return c.VolumeExistsByName(ctx, volConfig.InternalName)
768+
primaryName, secondaryName := gcnvVolumeFallbackNames(volConfig)
769+
770+
// Fall back to the creation token.
771+
exists, volume, err := c.VolumeExistsByName(ctx, primaryName)
772+
if err != nil {
773+
return false, nil, err
774+
}
775+
if exists {
776+
return true, volume, nil
777+
}
778+
779+
// For UNIFIED naming, also try underscore-normalized display name.
780+
if secondaryName == "" {
781+
return false, nil, nil
782+
}
783+
784+
return c.VolumeExistsByName(ctx, secondaryName)
739785
}
740786

741787
// VolumeByID returns a volume based on its full GCNV-style resource ID.

storage_drivers/gcp/api/gcnv_discovery.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ func (c Client) discoverCapacityPools(ctx context.Context) (*[]*CapacityPool, er
298298
NetworkFullName: pool.Network,
299299
Zone: pool.Zone,
300300
AutoTiering: pool.AllowAutoTiering,
301+
PoolType: pool.GetType().String(),
301302
})
302303
}
303304
}
@@ -327,13 +328,18 @@ func (c Client) CapacityPools() *[]*CapacityPool {
327328
// capacityPool returns a single discovered capacity pool by name.
328329
// Accepts either the short name (e.g., "pool-1") or the full name
329330
// (e.g., "projects/<project>/locations/<loc>/storagePools/<pool>").
331+
// Full-name lookups are O(1) via map; short names fall back to a linear scan.
330332
func (c Client) capacityPool(cPoolName string) *CapacityPool {
331333
if cPoolName == "" {
332334
return nil
333335
}
336+
cPools := c.sdkClient.resources.GetCapacityPools()
337+
if cPool, ok := cPools.GetOk(cPoolName); ok && cPool != nil {
338+
return cPool
339+
}
334340
var matchingCPool *CapacityPool
335-
c.sdkClient.resources.GetCapacityPools().Range(func(_ string, cPool *CapacityPool) bool {
336-
if cPool.Name == cPoolName || cPool.FullName == cPoolName {
341+
cPools.Range(func(_ string, cPool *CapacityPool) bool {
342+
if cPool.Name == cPoolName {
337343
matchingCPool = cPool
338344
return false
339345
}

storage_drivers/gcp/api/gcnv_discovery_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,35 @@ func TestCapacityPools(t *testing.T) {
204204
func TestCapacityPool_FullNameAndEmpty(t *testing.T) {
205205
sdk := getFakeSDK(true)
206206

207-
// Full name should resolve
207+
// Full name should resolve (O(1) map lookup) to same pool as short name (fallback scan)
208208
fullName := "projects/" + ProjectNumber + "/locations/" + Location + "/storagePools/CP1"
209209
assert.Equal(t, sdk.capacityPool("CP1"), sdk.capacityPool(fullName))
210210

211211
// Empty input should return nil
212212
assert.Nil(t, sdk.capacityPool(""))
213213
}
214214

215+
// TestCapacityPool_UnknownFullNameReturnsNil ensures that a full resource name not present
216+
// in the cache returns nil (map GetOk path), and does not match by short name.
217+
func TestCapacityPool_UnknownFullNameReturnsNil(t *testing.T) {
218+
sdk := getFakeSDK(true)
219+
220+
unknownFullName := "projects/" + ProjectNumber + "/locations/" + Location + "/storagePools/nonexistent"
221+
assert.Nil(t, sdk.capacityPool(unknownFullName))
222+
}
223+
224+
// TestCapacityPool_ShortNameFallback ensures that when the cache is keyed by full name,
225+
// lookup by short name (Name) still returns the pool via fallback scan.
226+
func TestCapacityPool_ShortNameFallback(t *testing.T) {
227+
sdk := getFakeSDK(true)
228+
229+
byShort := sdk.capacityPool("CP2")
230+
byFull := sdk.capacityPool("projects/" + ProjectNumber + "/locations/" + Location + "/storagePools/CP2")
231+
assert.NotNil(t, byShort)
232+
assert.Equal(t, byShort, byFull)
233+
assert.Equal(t, "CP2", byShort.Name)
234+
}
235+
215236
func TestCapacityPoolsForStoragePools(t *testing.T) {
216237
sdk := getFakeSDK(true)
217238
sdk.sdkClient.resources.SetStoragePools(make(map[string]storage.Pool))

storage_drivers/gcp/api/gcnv_sdk_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,37 @@ func TestNewVolumeFromGCNVVolume_ISCSIBlockDevicesAndPortals(t *testing.T) {
5252
assert.Equal(t, 0, vol.LunID)
5353
}
5454

55+
// TestNewVolumeFromGCNVVolume_ServiceLevelAndStoragePoolTypeFromSingleLookup verifies that
56+
// ServiceLevel and StoragePoolType are derived from a single capacity pool lookup (no duplicate scan),
57+
// and that StoragePoolType is empty when the pool is not found.
58+
func TestNewVolumeFromGCNVVolume_ServiceLevelAndStoragePoolTypeFromSingleLookup(t *testing.T) {
59+
sdk := getFakeSDK(true)
60+
cp1FullName := "projects/123456789/locations/fake-location/storagePools/CP1"
61+
62+
// Volume with StoragePool that resolves to CP1 (Premium); PoolType not set in getFakeSDK
63+
volPB := &netapppb.Volume{
64+
Name: "projects/123456789/locations/fake-location/volumes/vol1",
65+
Network: "projects/123456789/global/networks/default",
66+
StoragePool: cp1FullName,
67+
CapacityGib: 10,
68+
ShareName: "share1",
69+
Protocols: []netapppb.Protocols{netapppb.Protocols_NFSV3},
70+
}
71+
vol, err := sdk.newVolumeFromGCNVVolume(ctx, volPB)
72+
assert.NoError(t, err)
73+
assert.NotNil(t, vol)
74+
assert.Equal(t, ServiceLevelPremium, vol.ServiceLevel)
75+
assert.Equal(t, "", vol.StoragePoolType)
76+
77+
// When pool is not in cache, StoragePoolType must be empty (nil-safe)
78+
sdkNoPools := getFakeSDK()
79+
volPB.StoragePool = cp1FullName
80+
vol2, err := sdkNoPools.newVolumeFromGCNVVolume(ctx, volPB)
81+
assert.NoError(t, err)
82+
assert.NotNil(t, vol2)
83+
assert.Equal(t, "", vol2.StoragePoolType)
84+
}
85+
5586
func TestNewHostGroupFromGCNVHostGroup_Basic(t *testing.T) {
5687
sdk := getFakeSDK()
5788
hgPB := &netapppb.HostGroup{

storage_drivers/gcp/api/gcnv_structs.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ const (
5252
StoragePoolStateDisabled = "Disabled"
5353
StoragePoolStateError = "Error"
5454

55+
StoragePoolTypeFile = "FILE"
56+
StoragePoolTypeUnified = "UNIFIED"
57+
StoragePoolTypeUnifiedLargeCap = "UNIFIED_LARGE_CAPACITY"
58+
5559
VolumeStateUnspecified = "Unspecified"
5660
VolumeStateReady = "Ready"
5761
VolumeStateCreating = "Creating"
@@ -134,6 +138,12 @@ type CapacityPool struct {
134138
NetworkFullName string
135139
Zone string
136140
AutoTiering bool
141+
PoolType string
142+
}
143+
144+
// IsUnified returns true if the pool type is UNIFIED or UNIFIED_LARGE_CAPACITY.
145+
func (c *CapacityPool) IsUnified() bool {
146+
return c.PoolType == StoragePoolTypeUnified || c.PoolType == StoragePoolTypeUnifiedLargeCap
137147
}
138148

139149
// Volume records details of a discovered GCNV volume.
@@ -147,6 +157,7 @@ type Volume struct {
147157
NetworkName string
148158
NetworkFullName string
149159
ServiceLevel string
160+
StoragePoolType string
150161
SizeBytes int64
151162
ExportPolicy *ExportPolicy
152163
ProtocolTypes []string
@@ -166,6 +177,11 @@ type Volume struct {
166177
TieringMinimumCoolingDays *int32
167178
}
168179

180+
// IsUnified returns true if the volume resides on a UNIFIED or UNIFIED_LARGE_CAPACITY pool.
181+
func (v *Volume) IsUnified() bool {
182+
return v.StoragePoolType == StoragePoolTypeUnified || v.StoragePoolType == StoragePoolTypeUnifiedLargeCap
183+
}
184+
169185
// VolumeCreateRequest embodies all the details of a volume to be created.
170186
// This unified request supports both NAS (NFS/SMB) and block (iSCSI) volumes.
171187
type VolumeCreateRequest struct {

storage_drivers/gcp/api/gcnv_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,3 +1981,94 @@ func TestGetRegionalLocation(t *testing.T) {
19811981
assert.Contains(t, err.Error(), "backend config location is not set")
19821982
})
19831983
}
1984+
1985+
func TestCapacityPool_IsUnified(t *testing.T) {
1986+
tests := []struct {
1987+
PoolType string
1988+
Expected bool
1989+
}{
1990+
{StoragePoolTypeFile, false},
1991+
{StoragePoolTypeUnified, true},
1992+
{StoragePoolTypeUnifiedLargeCap, true},
1993+
}
1994+
for _, test := range tests {
1995+
t.Run(test.PoolType, func(t *testing.T) {
1996+
pool := &CapacityPool{
1997+
Name: "test-pool",
1998+
PoolType: test.PoolType,
1999+
}
2000+
2001+
result := pool.IsUnified()
2002+
2003+
assert.Equal(t, test.Expected, result, "IsUnified() mismatch for pool type %s", test.PoolType)
2004+
})
2005+
}
2006+
}
2007+
2008+
func TestVolume_IsUnified(t *testing.T) {
2009+
tests := []struct {
2010+
StoragePoolType string
2011+
Expected bool
2012+
}{
2013+
{StoragePoolTypeFile, false},
2014+
{StoragePoolTypeUnified, true},
2015+
{StoragePoolTypeUnifiedLargeCap, true},
2016+
}
2017+
for _, test := range tests {
2018+
t.Run(test.StoragePoolType, func(t *testing.T) {
2019+
vol := &Volume{
2020+
Name: "test-volume",
2021+
StoragePoolType: test.StoragePoolType,
2022+
}
2023+
2024+
result := vol.IsUnified()
2025+
2026+
assert.Equal(t, test.Expected, result, "IsUnified() mismatch for pool type %s", test.StoragePoolType)
2027+
})
2028+
}
2029+
}
2030+
2031+
func TestGCNVVolumeFallbackNames(t *testing.T) {
2032+
tests := []struct {
2033+
name string
2034+
volConfig *storage.VolumeConfig
2035+
expectedFirst string
2036+
expectedNext string
2037+
}{
2038+
{
2039+
name: "uses internal then underscore display variant",
2040+
volConfig: &storage.VolumeConfig{
2041+
InternalName: "my-vol",
2042+
Name: "my-vol",
2043+
},
2044+
expectedFirst: "my-vol",
2045+
expectedNext: "my_vol",
2046+
},
2047+
{
2048+
name: "no second lookup when same as internal",
2049+
volConfig: &storage.VolumeConfig{
2050+
InternalName: "my_vol",
2051+
Name: "my_vol",
2052+
},
2053+
expectedFirst: "my_vol",
2054+
expectedNext: "",
2055+
},
2056+
{
2057+
name: "no second lookup when display name empty",
2058+
volConfig: &storage.VolumeConfig{
2059+
InternalName: "token-1",
2060+
Name: "",
2061+
},
2062+
expectedFirst: "token-1",
2063+
expectedNext: "",
2064+
},
2065+
}
2066+
2067+
for _, test := range tests {
2068+
t.Run(test.name, func(t *testing.T) {
2069+
first, next := gcnvVolumeFallbackNames(test.volConfig)
2070+
assert.Equal(t, test.expectedFirst, first)
2071+
assert.Equal(t, test.expectedNext, next)
2072+
})
2073+
}
2074+
}

0 commit comments

Comments
 (0)