Skip to content

Commit 6aa2147

Browse files
authored
Add X-Dot-Client-App header to all ONTAP REST and ZAPI requests.
1 parent b53e1f5 commit 6aa2147

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

storage_drivers/ontap/api/azgo/common.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import (
2929
"github.com/netapp/trident/utils/errors"
3030
)
3131

32+
// ClientAppHeader is the HTTP header name ONTAP uses to identify the calling
33+
// application. Mirrored from the parent api package to avoid an import cycle.
34+
const ClientAppHeader = "X-Dot-Client-App"
35+
3236
type ZAPIRequest interface {
3337
ToXML() (string, error)
3438
}
@@ -242,6 +246,7 @@ func (o *ZapiRunner) SendZapiWithContext(ctx context.Context, r ZAPIRequest) (*h
242246
return nil, err
243247
}
244248
req.Header.Set("Content-Type", "application/xml")
249+
req.Header.Set(ClientAppHeader, "Trident/"+tridentconfig.OrchestratorVersion.ShortString())
245250
if o.ClientCertificate == "" || o.ClientPrivateKey == "" {
246251
req.SetBasicAuth(o.Username, o.Password)
247252
}

storage_drivers/ontap/api/azgo/common_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ func TestZapiRunner_ExecuteUsing_Parallel_MC(t *testing.T) {
154154
// Test parallel calls to ExecuteUsing with MC name changes
155155
zRunner := CreateTestZapiRunner(1)
156156
mockTransport := RoundTripFunc(func(req *http.Request) (*http.Response, error) {
157+
// Every outgoing ZAPI request must carry the X-Dot-Client-App header
158+
// so ONTAP can track per-app usage (TRID-19776).
159+
assert.NotEmpty(t, req.Header.Get(ClientAppHeader),
160+
"ZAPI request must include X-Dot-Client-App header")
161+
assert.True(t, strings.HasPrefix(req.Header.Get(ClientAppHeader), "Trident"),
162+
"X-Dot-Client-App header must start with 'Trident'")
157163
return &http.Response{
158164
StatusCode: http.StatusOK,
159165
Body: io.NopCloser(strings.NewReader(mockZapiErrorVserverNotFound)),
@@ -177,3 +183,27 @@ func TestZapiRunner_ExecuteUsing_Parallel_MC(t *testing.T) {
177183
// Check the SVM name, it should be one of the expected values
178184
assert.True(t, zRunner.svm == "svm-1-mc" || zRunner.svm == "svm-1")
179185
}
186+
187+
// TestSendZapiWithContext_SetsClientAppHeader verifies every ZAPI request
188+
// carries the X-Dot-Client-App header in the expected "Trident/<version>" format.
189+
func TestSendZapiWithContext_SetsClientAppHeader(t *testing.T) {
190+
zRunner := CreateTestZapiRunner(1)
191+
var gotHeader string
192+
mockTransport := RoundTripFunc(func(req *http.Request) (*http.Response, error) {
193+
gotHeader = req.Header.Get(ClientAppHeader)
194+
return &http.Response{
195+
StatusCode: http.StatusOK,
196+
Body: io.NopCloser(strings.NewReader(mockZapiErrorVserverNotFound)),
197+
Header: make(http.Header),
198+
}, nil
199+
})
200+
zRunner.httpClient = &http.Client{
201+
Transport: drivers.NewLimitedRetryTransport(semaphore.NewWeighted(1), mockTransport, ""),
202+
}
203+
204+
_, _ = zRunner.SendZapi(&VolumeCreateRequest{})
205+
assert.True(t, strings.HasPrefix(gotHeader, "Trident/"),
206+
"SendZapi must stamp the X-Dot-Client-App header as Trident/<version>, got: %s", gotHeader)
207+
assert.NotEqual(t, "Trident/", gotHeader,
208+
"X-Dot-Client-App header must include a non-empty version")
209+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2026 NetApp, Inc. All Rights Reserved.
2+
3+
package api
4+
5+
import (
6+
"net/http"
7+
8+
tridentconfig "github.com/netapp/trident/config"
9+
)
10+
11+
// ClientAppHeader is the HTTP header name ONTAP uses to identify the calling
12+
// application. ONTAP exposes it via `security session request-statistics
13+
// show-by-application`, allowing admins to track and rate-limit per-app usage.
14+
const ClientAppHeader = "X-Dot-Client-App"
15+
16+
// ClientAppHeaderValue returns the value to send in the X-Dot-Client-App header
17+
// for every REST/ZAPI request Trident issues to ONTAP, e.g. "Trident/26.06.0".
18+
func ClientAppHeaderValue() string {
19+
return "Trident/" + tridentconfig.OrchestratorVersion.ShortString()
20+
}
21+
22+
// ClientAppTransport is an http.RoundTripper that stamps the X-Dot-Client-App
23+
// header onto every outgoing request before delegating to a base transport.
24+
type ClientAppTransport struct {
25+
base http.RoundTripper
26+
value string
27+
}
28+
29+
// NewClientAppTransport wraps base so every request it sends carries the
30+
// X-Dot-Client-App header set to value.
31+
func NewClientAppTransport(base http.RoundTripper, value string) *ClientAppTransport {
32+
return &ClientAppTransport{base: base, value: value}
33+
}
34+
35+
// RoundTrip clones the request (per RoundTripper semantics) and sets the
36+
// X-Dot-Client-App header before handing off to the base transport.
37+
func (t *ClientAppTransport) RoundTrip(req *http.Request) (*http.Response, error) {
38+
r := req.Clone(req.Context())
39+
r.Header.Set(ClientAppHeader, t.value)
40+
return t.base.RoundTrip(r)
41+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2026 NetApp, Inc. All Rights Reserved.
2+
3+
package api
4+
5+
import (
6+
"io"
7+
"net/http"
8+
"strings"
9+
"sync/atomic"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
14+
tridentconfig "github.com/netapp/trident/config"
15+
)
16+
17+
// roundTripFunc adapts a function into an http.RoundTripper for tests.
18+
type roundTripFunc func(*http.Request) (*http.Response, error)
19+
20+
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
21+
22+
// TestClientAppHeaderValue verifies the computed header value matches the
23+
// documented format "Trident/<OrchestratorVersion.ShortString()>".
24+
func TestClientAppHeaderValue(t *testing.T) {
25+
want := "Trident/" + tridentconfig.OrchestratorVersion.ShortString()
26+
assert.Equal(t, want, ClientAppHeaderValue())
27+
assert.True(t, strings.HasPrefix(ClientAppHeaderValue(), "Trident/"),
28+
"header value must start with 'Trident/'")
29+
}
30+
31+
// TestClientAppHeaderName locks in the header name required by ONTAP.
32+
func TestClientAppHeaderName(t *testing.T) {
33+
assert.Equal(t, "X-Dot-Client-App", ClientAppHeader)
34+
}
35+
36+
// TestClientAppTransport_RoundTrip verifies the transport stamps the header
37+
// on every outgoing request.
38+
func TestClientAppTransport_RoundTrip(t *testing.T) {
39+
const want = "Trident/26.06.0"
40+
41+
var seen atomic.Int32
42+
base := roundTripFunc(func(r *http.Request) (*http.Response, error) {
43+
seen.Add(1)
44+
assert.Equal(t, want, r.Header.Get("X-Dot-Client-App"))
45+
return &http.Response{
46+
StatusCode: http.StatusOK,
47+
Body: io.NopCloser(strings.NewReader("")),
48+
Header: make(http.Header),
49+
}, nil
50+
})
51+
52+
tr := NewClientAppTransport(base, want)
53+
54+
for i := 0; i < 3; i++ {
55+
req, err := http.NewRequest(http.MethodGet, "http://example.invalid/api", nil)
56+
assert.NoError(t, err)
57+
resp, err := tr.RoundTrip(req)
58+
assert.NoError(t, err)
59+
assert.NotNil(t, resp)
60+
_ = resp.Body.Close()
61+
62+
// The caller's original request must not be mutated (RoundTripper contract).
63+
assert.Empty(t, req.Header.Get("X-Dot-Client-App"),
64+
"caller's request header must not be mutated")
65+
}
66+
67+
assert.Equal(t, int32(3), seen.Load(), "base transport should be called once per RoundTrip")
68+
}
69+
70+
// TestClientAppTransport_OverwritesExistingHeader verifies a pre-set header
71+
// on the incoming request is overwritten with our value (without mutating the
72+
// caller's request object).
73+
func TestClientAppTransport_OverwritesExistingHeader(t *testing.T) {
74+
const want = "Trident/26.06.1"
75+
76+
base := roundTripFunc(func(r *http.Request) (*http.Response, error) {
77+
assert.Equal(t, want, r.Header.Get("X-Dot-Client-App"))
78+
return &http.Response{
79+
StatusCode: http.StatusOK,
80+
Body: io.NopCloser(strings.NewReader("")),
81+
Header: make(http.Header),
82+
}, nil
83+
})
84+
85+
tr := NewClientAppTransport(base, want)
86+
req, err := http.NewRequest(http.MethodGet, "http://example.invalid/api", nil)
87+
assert.NoError(t, err)
88+
req.Header.Set("X-Dot-Client-App", "SomeOther/1.0")
89+
90+
resp, err := tr.RoundTrip(req)
91+
assert.NoError(t, err)
92+
_ = resp.Body.Close()
93+
94+
assert.Equal(t, "SomeOther/1.0", req.Header.Get("X-Dot-Client-App"),
95+
"caller's request header must be left alone")
96+
}

storage_drivers/ontap/api/ontap_rest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ func NewRestClient(ctx context.Context, config ClientConfig, SVM, driverName str
199199
transport = drivers.NewLimitedRetryTransport(
200200
drivers.NewSemaphore(config.ManagementLIF, drivers.ONTAPRequestLimit), transport, ContextRequestTargetONTAP,
201201
)
202+
// Stamp the X-Dot-Client-App header on every request so ONTAP can track per-app usage.
203+
transport = NewClientAppTransport(transport, ClientAppHeaderValue())
202204
result.httpClient = &http.Client{
203205
Transport: transport,
204206
}
@@ -215,6 +217,10 @@ func NewRestClient(ctx context.Context, config ClientConfig, SVM, driverName str
215217
}
216218
rClient.SetLogger(apiLogger)
217219
rClient.SetDebug(config.DebugTraceFlags["api"])
220+
// Also install our transport on the go-openapi runtime so the header is
221+
// applied to any REST call path that does not explicitly set
222+
// params.HTTPClient on the generated operation params.
223+
rClient.Transport = transport
218224
}
219225

220226
return result, nil

0 commit comments

Comments
 (0)