Skip to content

Commit d4a09f5

Browse files
leoromanovskyBridgeAR
authored andcommitted
fix(openfeature): allow custom agentless endpoints (#9481)
* fix(openfeature): support custom agentless endpoints * fix(openfeature): never send API keys to custom endpoints * fix(openfeature): clarify default endpoint error
1 parent c4b749d commit d4a09f5

5 files changed

Lines changed: 64 additions & 38 deletions

File tree

integration-tests/openfeature/openfeature-configuration-sources.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ function assertDeliveryTraffic (testCase) {
341341
for (const request of cdnRequests) {
342342
assert.strictEqual(request.url, `${AGENTLESS_PATH}?case=${testCase.identifier}`, testCase.label)
343343
assert.strictEqual(request.headers['accept-encoding'], 'gzip', testCase.label)
344-
assert.strictEqual(request.headers['dd-api-key'], 'integration-api-key', testCase.label)
344+
assert.strictEqual(request.headers['dd-api-key'], undefined, testCase.label)
345345
assert.strictEqual(request.headers['dd-client-library-language'], 'nodejs', testCase.label)
346346
assert.strictEqual(request.headers['dd-client-library-version'], VERSION, testCase.label)
347347
}

packages/dd-trace/src/openfeature/agentless_configuration_source.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const RETRY_JITTER = 0.2
2020
* @property {URL} endpoint
2121
* @property {number} pollIntervalMs
2222
* @property {number} requestTimeoutMs
23-
* @property {string} apiKey
23+
* @property {string | undefined} apiKey
2424
*/
2525

2626
/**
@@ -138,7 +138,7 @@ class AgentlessConfigurationSource {
138138
#request (signal) {
139139
const headers = getClientLibraryHeaders()
140140
headers['Accept-Encoding'] = 'gzip'
141-
headers['DD-API-KEY'] = this.#config.apiKey
141+
if (this.#config.apiKey) headers['DD-API-KEY'] = this.#config.apiKey
142142
if (this.#etag) headers['If-None-Match'] = this.#etag
143143

144144
/**
@@ -228,10 +228,7 @@ class AgentlessConfigurationSource {
228228
this.#failureWarnings.add(category)
229229

230230
if (statusCode === 401 || statusCode === 403) {
231-
log.warn(
232-
'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid',
233-
statusCode
234-
)
231+
log.warn('Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', statusCode)
235232
} else if (statusCode) {
236233
log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, attempts)
237234
} else if (attempts > 1) {

packages/dd-trace/src/openfeature/configuration_source.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const { isLoopbackHost } = require('../exporters/common/url')
43
const log = require('../log')
54

65
const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server'
@@ -27,17 +26,19 @@ function create (config, applyConfiguration) {
2726
return
2827
}
2928

29+
const hasCustomEndpoint = Boolean(baseUrl?.trim())
30+
3031
try {
31-
if (!config.DD_API_KEY) {
32-
throw new Error('DD_API_KEY is required for Feature Flagging agentless delivery')
32+
if (!hasCustomEndpoint && !config.DD_API_KEY) {
33+
throw new Error('DD_API_KEY is required for the default Datadog Feature Flagging endpoint')
3334
}
3435

3536
const AgentlessConfigurationSource = require('./agentless_configuration_source')
3637
return new AgentlessConfigurationSource({
3738
endpoint: endpoint(config, baseUrl),
3839
pollIntervalMs: Math.min(pollIntervalSeconds, MAX_POLL_INTERVAL_SECONDS) * 1000,
3940
requestTimeoutMs: requestTimeoutSeconds * 1000,
40-
apiKey: config.DD_API_KEY,
41+
apiKey: hasCustomEndpoint ? undefined : config.DD_API_KEY,
4142
}, applyConfiguration)
4243
} catch (error) {
4344
log.error('Unable to configure Feature Flagging configuration source', error)
@@ -74,10 +75,6 @@ function endpoint (config, configuredBaseUrl) {
7475
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
7576
throw new Error('Feature Flagging agentless URL must use HTTP or HTTPS')
7677
}
77-
if (url.protocol === 'http:' && !isLoopbackHost(url.hostname)) {
78-
throw new Error('Feature Flagging agentless URL must use HTTPS unless it targets loopback')
79-
}
80-
8178
if (url.pathname === '' || url.pathname === '/') {
8279
url.pathname = DEFAULT_AGENTLESS_PATH
8380
}

packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,15 @@ describe('AgentlessConfigurationSource', () => {
181181
clock.restore()
182182
clock = undefined
183183
const body = zlib.gzipSync(responseBody())
184-
nock('http://127.0.0.1:8080', {
184+
config.endpoint = new URL('http://flags.dev.internal:8080/custom/ufc')
185+
delete config.apiKey
186+
nock('http://flags.dev.internal:8080', {
187+
badheaders: ['dd-api-key'],
185188
reqheaders: {
186189
'accept-encoding': 'gzip',
187-
'dd-api-key': 'test-api-key',
188190
},
189191
})
190-
.get('/api/v2/feature-flagging/config/rules-based/server')
192+
.get('/custom/ufc')
191193
.reply(200, body, {
192194
'content-encoding': 'gzip',
193195
etag: '"real-path"',
@@ -414,7 +416,7 @@ describe('AgentlessConfigurationSource', () => {
414416
'third',
415417
])
416418
assert.deepStrictEqual(log.warn.thirdCall.args, [
417-
'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid',
419+
'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication',
418420
401,
419421
])
420422
})
@@ -480,7 +482,7 @@ describe('AgentlessConfigurationSource', () => {
480482
assert.strictEqual(requests.length, 1)
481483
sinon.assert.calledOnceWithExactly(
482484
log.warn,
483-
'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid',
485+
'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication',
484486
401
485487
)
486488

@@ -491,6 +493,22 @@ describe('AgentlessConfigurationSource', () => {
491493
sinon.assert.notCalled(applyConfiguration)
492494
})
493495

496+
it('omits a missing API key and reports the endpoint authentication failure', async () => {
497+
delete config.apiKey
498+
responses.push({ statusCode: 401 })
499+
500+
source().start()
501+
await flush()
502+
503+
assert.strictEqual(Object.hasOwn(requests[0].options.headers, 'DD-API-KEY'), false)
504+
sinon.assert.calledOnceWithExactly(
505+
log.warn,
506+
'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication',
507+
401
508+
)
509+
sinon.assert.notCalled(applyConfiguration)
510+
})
511+
494512
it('uses fixed-delay polling after a request completes', async () => {
495513
responses.push(
496514
{ pending: true },

packages/dd-trace/test/openfeature/configuration_source.spec.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,37 @@ describe('OpenFeature configuration source', () => {
8686
it(`allows the loopback endpoint ${baseUrl}`, () => {
8787
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = baseUrl
8888

89+
const resolved = createSourceConfig()
8990
assert.strictEqual(
90-
createSourceConfig().endpoint.toString(),
91+
resolved.endpoint.toString(),
9192
`${baseUrl}/api/v2/feature-flagging/config/rules-based/server`
9293
)
94+
assert.strictEqual(resolved.apiKey, undefined)
9395
})
9496
}
9597

98+
it('allows a cleartext custom endpoint for local development and proxies', () => {
99+
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL =
100+
'http://flags.dev.internal:8080'
101+
102+
const resolved = createSourceConfig()
103+
assert.strictEqual(
104+
resolved.endpoint.toString(),
105+
'http://flags.dev.internal:8080/api/v2/feature-flagging/config/rules-based/server'
106+
)
107+
assert.strictEqual(resolved.apiKey, undefined)
108+
})
109+
96110
it('preserves an exact configured path and query', () => {
97111
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL =
98112
'https://example.com/custom/ufc?tenant=one'
99113

114+
const resolved = createSourceConfig()
100115
assert.strictEqual(
101-
createSourceConfig().endpoint.toString(),
116+
resolved.endpoint.toString(),
102117
'https://example.com/custom/ufc?tenant=one'
103118
)
119+
assert.strictEqual(resolved.apiKey, undefined)
104120
})
105121

106122
it('derives and creates the managed GovCloud endpoint without hard-coding availability', () => {
@@ -156,20 +172,6 @@ describe('OpenFeature configuration source', () => {
156172
sinon.assert.notCalled(AgentlessConfigurationSource)
157173
})
158174

159-
it('rejects cleartext non-loopback endpoints', () => {
160-
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = 'http://flags.example.test'
161-
162-
const source = configurationSource.create(config, sinon.spy())
163-
164-
assert.strictEqual(source, undefined)
165-
sinon.assert.calledOnceWithMatch(
166-
log.error,
167-
'Unable to configure Feature Flagging configuration source',
168-
sinon.match.instanceOf(Error)
169-
)
170-
sinon.assert.notCalled(AgentlessConfigurationSource)
171-
})
172-
173175
it('rejects malformed endpoints without logging their sensitive value', () => {
174176
const sentinel = 'sensitive-value'
175177
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = `https://${sentinel} value`
@@ -187,15 +189,27 @@ describe('OpenFeature configuration source', () => {
187189
assert.strictEqual(log.error.firstCall.args[1].cause, undefined)
188190
})
189191

190-
it('requires an API key without enabling a source', () => {
192+
it('creates a source for a custom API without a Datadog API key', () => {
193+
delete config.DD_API_KEY
194+
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL =
195+
'https://flags.example.test/custom/ufc'
196+
197+
const source = configurationSource.create(config, sinon.spy())
198+
199+
assert.ok(source instanceof AgentlessConfigurationSource)
200+
assert.strictEqual(AgentlessConfigurationSource.firstCall.args[0].apiKey, undefined)
201+
sinon.assert.notCalled(log.error)
202+
})
203+
204+
it('requires a Datadog API key for the default Datadog Feature Flagging endpoint', () => {
191205
delete config.DD_API_KEY
192206

193207
const source = configurationSource.create(config, sinon.spy())
194208

195209
sinon.assert.calledOnceWithMatch(
196210
log.error,
197211
'Unable to configure Feature Flagging configuration source',
198-
sinon.match.instanceOf(Error)
212+
sinon.match.has('message', 'DD_API_KEY is required for the default Datadog Feature Flagging endpoint')
199213
)
200214
assert.strictEqual(source, undefined)
201215
sinon.assert.notCalled(AgentlessConfigurationSource)

0 commit comments

Comments
 (0)