Skip to content

Commit 6544a13

Browse files
Add GCNV ONTAP-mode HTTP transport for SAN and NAS drivers
1 parent 515cff8 commit 6544a13

32 files changed

Lines changed: 2542 additions & 54 deletions

logging/context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const (
7474
ContextRequestTargetTrident ContextRequestTarget = "trident"
7575
ContextRequestTargetKubernetes ContextRequestTarget = "kubernetes"
7676
ContextRequestTargetONTAP ContextRequestTarget = "ontap"
77+
ContextRequestTargetGCNV ContextRequestTarget = "gcnv"
7778
)
7879

7980
// Misc. context request values. These can be used with incoming and outgoing requests.

logging/patterns.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,21 @@ var backendAuthorization = redactedPattern{
5050
re: regexp.MustCompile(`\\*"username\\*":\\*"([^\\"])*\\*"`),
5151
rep: []byte("\\\"username\\\":<REDACTED>"),
5252
}
53+
54+
// backendMapOneLevelNested matches map[...] in %+#v logs with at most one nested map[...]
55+
// (e.g. credentialSource:map[file:...]). [^\]]* alone stops at the first ']' and leaks trailing fields.
56+
const backendMapOneLevelNested = `(?:[^\[\]]|map\[[^\]]*\])*`
57+
58+
// backendSensitiveMap redacts backend credential maps in map-formatted logs (%+#v).
59+
// Examples:
60+
//
61+
// credentials:map[name:my-secret type:secret]
62+
// apiKey:map[private_key:... private_key_id:...]
63+
// wipCredentialConfig:map[audience:... credentialSource:map[file:...] ...]
64+
// wipCredential:map[audience:... credentialSource:map[file:...] ...]
65+
var backendSensitiveMap = redactedPattern{
66+
re: regexp.MustCompile(
67+
`(credentials|apiKey|wipCredentialConfig|wipCredential):map\[` + backendMapOneLevelNested + `\]`,
68+
),
69+
rep: []byte("$1:<REDACTED>"),
70+
}

logging/patterns_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,57 @@ func TestBackendAuthorizationRedactor(t *testing.T) {
226226
})
227227
}
228228
}
229+
230+
func TestBackendSensitiveMapRedactor(t *testing.T) {
231+
tests := []struct {
232+
name string
233+
line string
234+
expected string
235+
}{
236+
{
237+
name: "credentials map",
238+
line: "credentials:map[name:my-secret type:secret]",
239+
expected: "credentials:<REDACTED>",
240+
},
241+
{
242+
name: "apiKey map",
243+
line: "apiKey:map[private_key:abc private_key_id:123]",
244+
expected: "apiKey:<REDACTED>",
245+
},
246+
{
247+
name: "wipCredentialConfig simple",
248+
line: "wipCredentialConfig:map[audience:a tokenURL:https://example/token]",
249+
expected: "wipCredentialConfig:<REDACTED>",
250+
},
251+
{
252+
name: "wipCredentialConfig nested map",
253+
line: "wipCredentialConfig:map[audience:a credentialSource:map[file:/var/run/secrets/token] " +
254+
"subjectTokenType:urn:ietf:params:oauth:token-type:jwt tokenURL:https://sts.googleapis.com type:external_account]",
255+
expected: "wipCredentialConfig:<REDACTED>",
256+
},
257+
{
258+
name: "wipCredential simple",
259+
line: "wipCredential:map[audience:a tokenURL:https://example/token]",
260+
expected: "wipCredential:<REDACTED>",
261+
},
262+
{
263+
name: "wipCredential nested map",
264+
line: "wipCredential:map[audience:a credentialSource:map[file:/var/run/secrets/token] " +
265+
"subjectTokenType:urn:ietf:params:oauth:token-type:jwt tokenURL:https://sts.googleapis.com type:external_account]",
266+
expected: "wipCredential:<REDACTED>",
267+
},
268+
{
269+
name: "wrong key unchanged",
270+
line: "wipCredentialConfigs:map[audience:a]",
271+
expected: "wipCredentialConfigs:map[audience:a]",
272+
},
273+
}
274+
275+
for _, test := range tests {
276+
t.Run(test.name, func(t *testing.T) {
277+
input := []byte(test.line)
278+
actual := backendSensitiveMap.re.ReplaceAll(input, backendSensitiveMap.rep)
279+
assert.Equal(t, test.expected, string(actual))
280+
})
281+
}
282+
}

logging/redactor.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var redactedPatterns = []redactedPattern{
2020
backendCreateCHAPSecrets,
2121
backendCreateCHAPUsername,
2222
backendAuthorization,
23+
backendSensitiveMap,
2324
}
2425

2526
// Redactor is a formatter that redacts pre-defined regex patterns
@@ -32,6 +33,16 @@ func (r *Redactor) Format(entry *log.Entry) ([]byte, error) {
3233
return redactAllPatterns(line), err
3334
}
3435

36+
// RedactBytes applies all configured logging redaction patterns to raw bytes.
37+
func RedactBytes(line []byte) []byte {
38+
return redactAllPatterns(line)
39+
}
40+
41+
// RedactString applies all configured logging redaction patterns to a string.
42+
func RedactString(line string) string {
43+
return string(redactAllPatterns([]byte(line)))
44+
}
45+
3546
func redactAllPatterns(line []byte) []byte {
3647
for _, rp := range redactedPatterns {
3748
line = rp.re.ReplaceAll(line, rp.rep)

logging/transport.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ func (m *MetricsTransport) RoundTrip(req *http.Request) (*http.Response, error)
6161
recErr = err
6262
// Handle the special ONTAP case where the API may return EOF instead of
6363
// an HTTP backpressure status when the server gives up.
64-
if errors.Is(err, io.EOF) && m.target == ContextRequestTargetONTAP {
64+
if errors.Is(err, io.EOF) &&
65+
(m.target == ContextRequestTargetONTAP || m.target == ContextRequestTargetGCNV) {
6566
recErr = errors.WrapWithServerBackPressureError(err, "received EOF from server")
6667
}
6768
return res, recErr
@@ -77,10 +78,10 @@ func (m *MetricsTransport) RoundTrip(req *http.Request) (*http.Response, error)
7778
recErr = errors.ServerBackPressureError("received status: %d from the server", res.StatusCode)
7879

7980
switch m.target {
80-
case ContextRequestTargetONTAP:
81+
case ContextRequestTargetONTAP, ContextRequestTargetGCNV:
8182
// This breaks the standard RoundTripper semantics but is necessary to handle
82-
// the special case where ONTAP returns an EOF error when the API is too busy.
83-
// ONTAP callers gate on err != nil and may not inspect the HTTP status code,
83+
// the special case where ONTAP/GCNV proxy returns an EOF error when the API is too busy.
84+
// Callers gate on err != nil and may not inspect the HTTP status code,
8485
// so return a non-nil backpressure error.
8586
return res, recErr
8687
case ContextRequestTargetKubernetes:

persistent_store/crd/apis/netapp/v1/backendconfig.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,7 @@ func (s *TridentBackendConfigSpec) ToString() string {
8484
return ""
8585
}
8686

87-
// Redact the credentials information
88-
backendConfigSpec["credentials"] = "<REDACTED>"
89-
90-
return fmt.Sprintf("backendConfig: %+v", backendConfigSpec)
87+
return RedactString(fmt.Sprintf("backendConfig: %+v", backendConfigSpec))
9188
}
9289

9390
func (in *TridentBackendConfig) IsSpecValid() bool {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2026 NetApp, Inc. All Rights Reserved.
2+
3+
package v1
4+
5+
import (
6+
"encoding/json"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
)
12+
13+
func TestTridentBackendConfigSpec_ToString_redactsGCNVAPIKey(t *testing.T) {
14+
specJSON := `{
15+
"version": 1,
16+
"storageDriverName": "ontap-nas",
17+
"credentials": {"name": "gcnv-sa-secret", "type": "secret"},
18+
"gcnv": {
19+
"proxyURL": "https://netapp.googleapis.com",
20+
"apiKey": {
21+
"type": "service_account",
22+
"private_key": "FAKE-PRIVATE-KEY-VALUE",
23+
"private_key_id": "key-id-123"
24+
},
25+
"wipCredential": {"audience": "test", "serviceAccountEmail": "sa@test"}
26+
}
27+
}`
28+
29+
spec := &TridentBackendConfigSpec{
30+
RawExtension: runtime.RawExtension{Raw: json.RawMessage(specJSON)},
31+
}
32+
out := spec.ToString()
33+
34+
require.NotContains(t, out, "SECRET")
35+
require.NotContains(t, out, "key-id-123")
36+
require.NotContains(t, out, "gcnv-sa-secret")
37+
require.Contains(t, out, "credentials:<REDACTED>")
38+
require.Contains(t, out, "apiKey:<REDACTED>")
39+
require.Contains(t, out, "wipCredential:<REDACTED>")
40+
require.Contains(t, out, "proxyURL:https://netapp.googleapis.com")
41+
}
42+
43+
func TestTridentBackendConfigSpec_ToString_redactsNativeGCNVAPIKey(t *testing.T) {
44+
specJSON := `{
45+
"version": 1,
46+
"storageDriverName": "gcnv-nas",
47+
"apiKey": {"private_key": "native-secret", "private_key_id": "native-id"},
48+
"wipCredentialConfig": {"audience": "a"}
49+
}`
50+
51+
spec := &TridentBackendConfigSpec{
52+
RawExtension: runtime.RawExtension{Raw: json.RawMessage(specJSON)},
53+
}
54+
out := spec.ToString()
55+
56+
require.NotContains(t, out, "native-secret")
57+
require.NotContains(t, out, "native-id")
58+
require.Contains(t, out, "apiKey:<REDACTED>")
59+
require.Contains(t, out, "wipCredentialConfig:<REDACTED>")
60+
}
61+
62+
func TestTridentBackendConfigSpec_ToString_redactsNestedWIPCredential(t *testing.T) {
63+
specJSON := `{
64+
"version": 1,
65+
"storageDriverName": "ontap-nas",
66+
"gcnv": {
67+
"proxyURL": "https://netapp.googleapis.com",
68+
"wipCredential": {
69+
"audience": "//iam.googleapis.com/projects/123",
70+
"credentialSource": {"file": "/var/run/secrets/token"},
71+
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
72+
"tokenURL": "https://sts.googleapis.com/v1/token",
73+
"type": "external_account"
74+
}
75+
}
76+
}`
77+
78+
spec := &TridentBackendConfigSpec{
79+
RawExtension: runtime.RawExtension{Raw: json.RawMessage(specJSON)},
80+
}
81+
out := spec.ToString()
82+
83+
require.NotContains(t, out, "/var/run/secrets/token")
84+
require.NotContains(t, out, "sts.googleapis.com")
85+
require.Contains(t, out, "wipCredential:<REDACTED>")
86+
}

storage_drivers/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
var ontapConfigRedactList = [...]string{
3535
"Username", "Password", "ChapUsername", "ChapInitiatorSecret",
3636
"ChapTargetUsername", "ChapTargetInitiatorSecret", "ClientPrivateKey",
37+
"GCNVConfig",
3738
}
3839

3940
func GetOntapConfigRedactList() []string {

storage_drivers/ontap/api/abstraction.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ type OntapAPI interface {
202202
VolumeCloneSplitStart(ctx context.Context, cloneName string) error
203203

204204
VolumeCreate(ctx context.Context, volume Volume) error
205+
// VolumeDestroy keeps both flags in the shared abstraction because ZAPI and REST expose
206+
// delete semantics differently. ZAPI consumes force directly and may need a follow-up
207+
// recovery-queue purge when skipRecoveryQueue is true. REST only exposes one delete flag at
208+
// the transport layer, so the REST adapter maps skipRecoveryQueue onto that flag while keeping
209+
// this caller-facing signature stable.
205210
VolumeDestroy(ctx context.Context, volumeName string, force, skipRecoveryQueue bool) error
206211
VolumeModifySnapshotDirectoryAccess(ctx context.Context, name string, enable bool) error
207212
VolumeExists(ctx context.Context, volumeName string) (bool, error)

storage_drivers/ontap/api/abstraction_zapi.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ func (d OntapAPIZAPI) VolumeCreate(ctx context.Context, volume Volume) error {
9696
return err
9797
}
9898

99+
// VolumeDestroy deletes a flexvol via ONTAP ZAPI.
100+
//
101+
// force and skipRecoveryQueue are not the same on ZAPI:
102+
// - force: passed to ZAPI VolumeDestroy (ONTAP force delete semantics).
103+
// - skipRecoveryQueue: when true, after a successful destroy, best-effort purge of the volume
104+
// from the recovery queue via separate ZAPI calls (GetIter + Purge).
105+
//
106+
// REST/GCNV uses abstraction_rest.VolumeDestroy instead; there skipRecoveryQueue maps to REST
107+
// DELETE ?force= and there is no post-delete RQ purge in that path.
99108
func (d OntapAPIZAPI) VolumeDestroy(ctx context.Context, name string, force, skipRecoveryQueue bool) error {
100109
volDestroyResponse, err := d.api.VolumeDestroy(name, force)
101110
if err != nil {

0 commit comments

Comments
 (0)