Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load From CDN #123

Open
wants to merge 45 commits into
base: release/v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ce9f550
use pipeline policy to ensure cdn request uses correct api version
zhiyuanliang-ms Nov 4, 2024
2adadc7
fix lint & add comments
zhiyuanliang-ms Nov 4, 2024
30108ab
update
zhiyuanliang-ms Nov 5, 2024
197db89
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 5, 2024
04d8a2c
update
zhiyuanliang-ms Nov 7, 2024
bf99b31
fix lint
zhiyuanliang-ms Nov 7, 2024
14364c5
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 10, 2024
62ac2b9
add request tracing for cdn
zhiyuanliang-ms Nov 10, 2024
bd1c875
only send conditional request when cdn is not used
zhiyuanliang-ms Nov 14, 2024
0856707
add testcase
zhiyuanliang-ms Nov 14, 2024
2b78b27
fix lint
zhiyuanliang-ms Nov 15, 2024
fdd30e2
refresh based on page etag
zhiyuanliang-ms Nov 18, 2024
c886936
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 18, 2024
2a7399b
merge preview
zhiyuanliang-ms Nov 18, 2024
b4afe7a
merge preview
zhiyuanliang-ms Nov 18, 2024
50fc5b6
remove watchAll & reorganize the code
zhiyuanliang-ms Nov 19, 2024
70bbdee
add testcase
zhiyuanliang-ms Nov 19, 2024
d8103a5
fix lint & update method name
zhiyuanliang-ms Nov 19, 2024
e123f0e
resolve merge conflict
zhiyuanliang-ms Nov 19, 2024
6958ad5
add comment
zhiyuanliang-ms Nov 19, 2024
635ca48
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Nov 19, 2024
61e4a65
not use conditional request
zhiyuanliang-ms Nov 19, 2024
4f36a1c
fix vulnerability
zhiyuanliang-ms Nov 20, 2024
d325127
resolve merge conflict
zhiyuanliang-ms Dec 2, 2024
647c7a6
resolve conflict
zhiyuanliang-ms Dec 2, 2024
b74e983
update variable name
zhiyuanliang-ms Dec 2, 2024
eec7114
merge
zhiyuanliang-ms Dec 2, 2024
01a7034
move public method
zhiyuanliang-ms Dec 2, 2024
0fb81da
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Dec 2, 2024
b0ab944
append etag to url
zhiyuanliang-ms Dec 3, 2024
c094801
update
zhiyuanliang-ms Dec 4, 2024
bbf1938
update
zhiyuanliang-ms Dec 4, 2024
f3ac831
add more comments
zhiyuanliang-ms Dec 5, 2024
e6fea3c
merge preview
zhiyuanliang-ms Dec 13, 2024
072bcdf
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Dec 13, 2024
356e664
fix lint
zhiyuanliang-ms Dec 13, 2024
2b95ab4
resolve merge conflict
zhiyuanliang-ms Dec 13, 2024
d3c2799
resolve merge conflict
zhiyuanliang-ms Dec 18, 2024
290d338
merge preview
zhiyuanliang-ms Dec 18, 2024
ea7709e
merge
zhiyuanliang-ms Dec 18, 2024
8ed48b5
merge preview
zhiyuanliang-ms Dec 19, 2024
6e34f62
resolve merge conflict
zhiyuanliang-ms Dec 19, 2024
14dbe96
update
zhiyuanliang-ms Dec 19, 2024
8441b32
resolve copilot comment
zhiyuanliang-ms Dec 19, 2024
6fe042a
fix lint
zhiyuanliang-ms Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"uuid": "^9.0.1"
},
"dependencies": {
"@azure/app-configuration": "^1.6.1",
"@azure/app-configuration": "^1.8.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0"
}
Expand Down
165 changes: 121 additions & 44 deletions src/AzureAppConfigurationImpl.ts

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions src/EtagUrlPipelinePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { PipelinePolicy } from "@azure/core-rest-pipeline";

export const ETAG_LOOKUP_HEADER = "Etag-Lookup";

/**
* The pipeline policy that retrieves the etag from the request header and appends it to the request URL. After that the etag header is removed from the request.
* @remarks
* The policy position should be perCall.
* The App Configuration service will not recognize the etag query parameter in the url, but this can help to break the CDN cache as the cache entry is based on the URL.
*/
export class EtagUrlPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationEtagUrlPolicy";

async sendRequest(request, next) {
if (request.headers.has(ETAG_LOOKUP_HEADER)) {
const etag = request.headers.get(ETAG_LOOKUP_HEADER);
request.headers.delete(ETAG_LOOKUP_HEADER);

const url = new URL(request.url);
url.searchParams.append("_", etag); // _ is a dummy query parameter to break the CDN cache
request.url = url.toString();
}

return next(request);
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

export { AzureAppConfiguration } from "./AzureAppConfiguration.js";
export { Disposable } from "./common/disposable.js";
export { load } from "./load.js";
export { load, loadFromCdn } from "./load.js";
export { KeyFilter, LabelFilter } from "./types.js";
export { VERSION } from "./version.js";
37 changes: 36 additions & 1 deletion src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { AzureAppConfiguration } from "./AzureAppConfiguration.js";
import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
import { ConfigurationClientManager, instanceOfTokenCredential } from "./ConfigurationClientManager.js";
import { EtagUrlPipelinePolicy } from "./EtagUrlPipelinePolicy.js";

const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds

// Empty token credential to be used when loading from CDN
const emptyTokenCredential: TokenCredential = {
getToken: async () => ({ token: "", expiresOnTimestamp: 0 })
};

/**
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.
* @param connectionString The connection string for the App Configuration store.
Expand Down Expand Up @@ -41,7 +47,7 @@ export async function load(
}

try {
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options);
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options, credentialOrOptions === emptyTokenCredential);
await appConfiguration.load();
return appConfiguration;
} catch (error) {
Expand All @@ -55,3 +61,32 @@ export async function load(
throw error;
}
}

/**
* Loads the data from a CDN and returns an instance of AzureAppConfiguration.
* @param cdnEndpoint The URL to the CDN.
* @param appConfigOptions Optional parameters.
*/
export async function loadFromCdn(cdnEndpoint: URL | string, options?: AzureAppConfigurationOptions): Promise<AzureAppConfiguration>;

export async function loadFromCdn(
cdnEndpoint: string | URL,
appConfigOptions?: AzureAppConfigurationOptions
): Promise<AzureAppConfiguration> {
if (appConfigOptions === undefined) {
appConfigOptions = { clientOptions: {}};
}

appConfigOptions.clientOptions = {
...appConfigOptions.clientOptions,
// Specify the api version that supports sas token authentication
apiVersion: "2024-09-01-preview",
// Add etag url policy to append etag to the request url for breaking CDN cache
additionalPolicies: [
...(appConfigOptions.clientOptions?.additionalPolicies || []),
{ policy: new EtagUrlPipelinePolicy(), position: "perCall" }
]
};

return await load(cdnEndpoint, emptyTokenCredential, appConfigOptions);
}
1 change: 1 addition & 0 deletions src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const REPLICA_COUNT_KEY = "ReplicaCount";

// Tag names
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
export const CDN_USED_TAG = "CDN";
export const FAILOVER_REQUEST_TAG = "Failover";

// Compact feature tags
Expand Down
8 changes: 8 additions & 0 deletions src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
HOST_TYPE_KEY,
HostType,
KEY_VAULT_CONFIGURED_TAG,
CDN_USED_TAG,
KUBERNETES_ENV_VAR,
NODEJS_DEV_ENV_VAL,
NODEJS_ENV_VAR,
Expand All @@ -36,6 +37,7 @@ export interface RequestTracingOptions {
initialLoadCompleted: boolean;
replicaCount: number;
isFailoverRequest: boolean;
isCdnUsed: boolean;
featureFlagTracing: FeatureFlagTracingOptions | undefined;
}

Expand All @@ -48,6 +50,7 @@ export function listConfigurationSettingsWithTrace(
const actualListOptions = { ...listOptions };
if (requestTracingOptions.enabled) {
actualListOptions.requestOptions = {
...actualListOptions.requestOptions,
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
}
Expand All @@ -67,6 +70,7 @@ export function getConfigurationSettingWithTrace(

if (requestTracingOptions.enabled) {
actualGetOptions.requestOptions = {
...actualGetOptions.requestOptions,
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
}
Expand All @@ -88,6 +92,7 @@ export function createCorrelationContextHeader(requestTracingOptions: RequestTra
FFFeatures: Seed+Telemetry
UsersKeyVault
Failover
CDN
*/
const keyValues = new Map<string, string | undefined>();
const tags: string[] = [];
Expand Down Expand Up @@ -116,6 +121,9 @@ export function createCorrelationContextHeader(requestTracingOptions: RequestTra
if (requestTracingOptions.isFailoverRequest) {
tags.push(FAILOVER_REQUEST_TAG);
}
if (requestTracingOptions.isCdnUsed) {
tags.push(CDN_USED_TAG);
}
if (requestTracingOptions.replicaCount > 0) {
keyValues.set(REPLICA_COUNT_KEY, requestTracingOptions.replicaCount.toString());
}
Expand Down
2 changes: 1 addition & 1 deletion test/exportedApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export { load } from "../src";
export { load, loadFromCdn } from "../src";
15 changes: 10 additions & 5 deletions test/loadBalance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import * as chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
const expect = chai.expect;
import { load } from "./exportedApi.js";
import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
import { AppConfigurationClient } from "@azure/app-configuration";
import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js";

const mockedKVs = [
{ value: "red", key: "app.settings.fontColor" },
{ value: "40", key: "app.settings.fontSize" },
{ value: "30", key: "app.settings.fontSize", label: "prod" }
].map(createMockedKeyValue);
const fakeEndpoint_1 = createMockedEndpoint("fake_1");
const fakeEndpoint_2 = createMockedEndpoint("fake_2");
const fakeClientWrapper_1 = new ConfigurationClientWrapper(fakeEndpoint_1, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_1)));
Expand All @@ -29,8 +34,8 @@ describe("load balance", function () {

it("should load balance the request when loadBalancingEnabled", async () => {
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_2, clientRequestCounter_2);

const connectionString = createMockedConnectionString();
const settings = await load(connectionString, {
Expand Down Expand Up @@ -66,8 +71,8 @@ describe("load balance", function () {
clientRequestCounter_1.count = 0;
clientRequestCounter_2.count = 0;
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_2, clientRequestCounter_2);

const connectionString = createMockedConnectionString();
// loadBalancingEnabled is default to false
Expand Down
26 changes: 25 additions & 1 deletion test/requestTracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ chai.use(chaiAsPromised);
const expect = chai.expect;
import { MAX_TIME_OUT, HttpRequestHeadersPolicy, createMockedConnectionString, createMockedKeyValue, createMockedFeatureFlag, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sinon, sleepInMs } from "./utils/testHelper.js";
import { ConfigurationClientManager } from "../src/ConfigurationClientManager.js";
import { load } from "./exportedApi.js";
import { load, loadFromCdn } from "./exportedApi.js";

const CORRELATION_CONTEXT_HEADER_NAME = "Correlation-Context";

Expand Down Expand Up @@ -77,6 +77,30 @@ describe("request tracing", function () {
sinon.restore();
});

it("should have cdn tag in correlation-context header when loadFromCdn is used", async () => {
try {
await loadFromCdn(fakeEndpoint, {
clientOptions
});
} catch (e) { /* empty */ }
expect(headerPolicy.headers).not.undefined;
const correlationContext = headerPolicy.headers.get("Correlation-Context");
expect(correlationContext).not.undefined;
expect(correlationContext.includes("CDN")).eq(true);
});

it("should not have cdn tag in correlation-context header when load is used", async () => {
try {
await load(createMockedConnectionString(fakeEndpoint), {
clientOptions
});
} catch (e) { /* empty */ }
expect(headerPolicy.headers).not.undefined;
const correlationContext = headerPolicy.headers.get("Correlation-Context");
expect(correlationContext).not.undefined;
expect(correlationContext.includes("CDN")).eq(false);
});

it("should detect env in correlation-context header", async () => {
process.env.NODE_ENV = "development";
try {
Expand Down
7 changes: 3 additions & 4 deletions test/utils/testHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,11 @@ function mockAppConfigurationClientListConfigurationSettings(pages: Configuratio
});
}

function mockAppConfigurationClientLoadBalanceMode(clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
const emptyPages: ConfigurationSetting[][] = [];
function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[][], clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
sinon.stub(clientWrapper.client, "listConfigurationSettings").callsFake((listOptions) => {
countObject.count += 1;
const kvs = _filterKVs(emptyPages.flat(), listOptions);
return getMockedIterator(emptyPages, kvs, listOptions);
const kvs = _filterKVs(pages.flat(), listOptions);
return getMockedIterator(pages, kvs, listOptions);
});
}

Expand Down
Loading