Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 17 additions & 16 deletions src/appConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ import { AIConfigurationTracingOptions } from "./requestTracing/aiConfigurationT
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
import { ConfigurationClientManager } from "./configurationClientManager.js";
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/error.js";
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js";
import { ErrorMessages } from "./common/errorMessages.js";

const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds

Expand Down Expand Up @@ -151,10 +152,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
} else {
for (const setting of watchedSettings) {
if (setting.key.includes("*") || setting.key.includes(",")) {
throw new ArgumentError("The characters '*' and ',' are not supported in key of watched settings.");
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_KEY);
}
if (setting.label?.includes("*") || setting.label?.includes(",")) {
throw new ArgumentError("The characters '*' and ',' are not supported in label of watched settings.");
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_LABEL);
}
this.#sentinels.push(setting);
}
Expand All @@ -163,7 +164,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
// custom refresh interval
if (refreshIntervalInMs !== undefined) {
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
throw new RangeError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
throw new RangeError(ErrorMessages.INVALID_REFRESH_INTERVAL);
}
this.#kvRefreshInterval = refreshIntervalInMs;
}
Expand All @@ -182,7 +183,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
// custom refresh interval
if (refreshIntervalInMs !== undefined) {
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
throw new RangeError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
throw new RangeError(ErrorMessages.INVALID_FEATURE_FLAG_REFRESH_INTERVAL);
}
this.#ffRefreshInterval = refreshIntervalInMs;
}
Expand All @@ -195,7 +196,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
const { secretRefreshIntervalInMs } = options.keyVaultOptions;
if (secretRefreshIntervalInMs !== undefined) {
if (secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS) {
throw new RangeError(`The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`);
throw new RangeError(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL);
}
this.#secretRefreshEnabled = true;
this.#secretRefreshTimer = new RefreshTimer(secretRefreshIntervalInMs);
Expand Down Expand Up @@ -272,7 +273,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
new Promise((_, reject) => {
timeoutId = setTimeout(() => {
abortController.abort(); // abort the initialization promise
reject(new Error("Load operation timed out."));
reject(new Error(ErrorMessages.LOAD_OPERATION_TIMEOUT));
},
startupTimeout);
})
Expand All @@ -287,7 +288,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
await new Promise(resolve => setTimeout(resolve, MIN_DELAY_FOR_UNHANDLED_FAILURE - timeElapsed));
}
}
throw new Error("Failed to load.", { cause: error });
throw new Error(ErrorMessages.LOAD_OPERATION_FAILED, { cause: error });
} finally {
clearTimeout(timeoutId); // cancel the timeout promise
}
Expand Down Expand Up @@ -341,7 +342,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
*/
async refresh(): Promise<void> {
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED);
}

if (this.#refreshInProgress) {
Expand All @@ -360,7 +361,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
*/
onRefresh(listener: () => any, thisArg?: any): Disposable {
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
throw new InvalidOperationError(ErrorMessages.REFRESH_NOT_ENABLED);
}

const boundedListener = listener.bind(thisArg);
Expand Down Expand Up @@ -840,7 +841,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}

this.#clientManager.refreshClients();
throw new Error("All fallback clients failed to get configuration settings.");
throw new Error(ErrorMessages.ALL_FALLBACK_CLIENTS_FAILED);
}

async #resolveSecretReferences(secretReferences: ConfigurationSetting[], resultHandler: (key: string, value: unknown) => void): Promise<void> {
Expand Down Expand Up @@ -916,7 +917,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
async #parseFeatureFlag(setting: ConfigurationSetting<string>): Promise<any> {
const rawFlag = setting.value;
if (rawFlag === undefined) {
throw new ArgumentError("The value of configuration setting cannot be undefined.");
throw new ArgumentError(ErrorMessages.CONFIGURATION_SETTING_VALUE_UNDEFINED);
}
const featureFlag = JSON.parse(rawFlag);

Expand Down Expand Up @@ -983,17 +984,17 @@ function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector
const selector = { ...selectorCandidate };
if (selector.snapshotName) {
if (selector.keyFilter || selector.labelFilter || selector.tagFilters) {
throw new ArgumentError("Key, label or tag filters should not be specified while selecting a snapshot.");
throw new ArgumentError(ErrorMessages.INVALID_SNAPSHOT_SELECTOR);
}
} else {
if (!selector.keyFilter) {
throw new ArgumentError("Key filter cannot be null or empty.");
throw new ArgumentError(ErrorMessages.INVALID_KEY_FILTER);
}
if (!selector.labelFilter) {
selector.labelFilter = LabelFilter.Null;
}
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
throw new ArgumentError(ErrorMessages.INVALID_LABEL_FILTER);
}
if (selector.tagFilters) {
validateTagFilters(selector.tagFilters);
Expand Down Expand Up @@ -1045,7 +1046,7 @@ function validateTagFilters(tagFilters: string[]): void {
for (const tagFilter of tagFilters) {
const res = tagFilter.split("=");
if (res[0] === "" || res.length !== 2) {
throw new Error(`Invalid tag filter: ${tagFilter}. Tag filter must follow the format "tagName=tagValue".`);
throw new Error(`Invalid tag filter: ${tagFilter}. ${ErrorMessages.INVALID_TAG_FILTER}.`);
}
}
}
28 changes: 28 additions & 0 deletions src/common/errorMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { MIN_REFRESH_INTERVAL_IN_MS } from "../refresh/refreshOptions.js";
import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "../keyvault/keyVaultOptions.js";

export const enum ErrorMessages {
INVALID_WATCHED_SETTINGS_KEY = "The characters '*' and ',' are not supported in key of watched settings.",
INVALID_WATCHED_SETTINGS_LABEL = "The characters '*' and ',' are not supported in label of watched settings.",
INVALID_REFRESH_INTERVAL = `The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`,
INVALID_FEATURE_FLAG_REFRESH_INTERVAL = `The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`,
INVALID_SECRET_REFRESH_INTERVAL = `The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`,
LOAD_OPERATION_TIMEOUT = "The load operation timed out.",
LOAD_OPERATION_FAILED = "The load operation failed.",
REFRESH_NOT_ENABLED = "Refresh is not enabled for key-values, feature flags or Key Vault secrets.",
ALL_FALLBACK_CLIENTS_FAILED = "All fallback clients failed to get configuration settings.",
CONFIGURATION_SETTING_VALUE_UNDEFINED = "The value of configuration setting cannot be undefined.",
INVALID_SNAPSHOT_SELECTOR = "Key, label or tag filters should not be specified while selecting a snapshot.",
INVALID_KEY_FILTER = "Key filter cannot be null or empty.",
INVALID_LABEL_FILTER = "The characters '*' and ',' are not supported in label filters.",
INVALID_TAG_FILTER = "Tag filter must follow the format 'tagName=tagValue'",
CONNECTION_STRING_OR_ENDPOINT_MISSED = "A connection string or an endpoint with credential must be specified to create a client.",
}

export const enum KeyVaultReferenceErrorMessages {
KEY_VAULT_OPTIONS_UNDEFINED = "Failed to process the Key Vault reference because Key Vault options are not configured.",
KEY_VAULT_REFERENCE_UNRESOLVABLE = "Failed to resolve the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret."
}
File renamed without changes.
5 changes: 3 additions & 2 deletions src/configurationClientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
import * as RequestTracing from "./requestTracing/constants.js";
import { shuffleList, instanceOfTokenCredential } from "./common/utils.js";
import { ArgumentError } from "./common/error.js";
import { ArgumentError } from "./common/errors.js";
import { ErrorMessages } from "./common/errorMessages.js";

// Configuration client retry options
const CLIENT_MAX_RETRIES = 2;
Expand Down Expand Up @@ -80,7 +81,7 @@ export class ConfigurationClientManager {
this.#credential = credential;
staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions);
} else {
throw new ArgumentError("A connection string or an endpoint with credential must be specified to create a client.");
throw new ArgumentError(ErrorMessages.CONNECTION_STRING_OR_ENDPOINT_MISSED);
}

this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)];
Expand Down
5 changes: 3 additions & 2 deletions src/keyvault/keyVaultKeyValueAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { IKeyValueAdapter } from "../keyValueAdapter.js";
import { AzureKeyVaultSecretProvider } from "./keyVaultSecretProvider.js";
import { KeyVaultOptions } from "./keyVaultOptions.js";
import { RefreshTimer } from "../refresh/refreshTimer.js";
import { ArgumentError, KeyVaultReferenceError } from "../common/error.js";
import { ArgumentError, KeyVaultReferenceError } from "../common/errors.js";
import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js";
import { KeyVaultSecretIdentifier, parseKeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
import { isRestError } from "@azure/core-rest-pipeline";
import { AuthenticationError } from "@azure/identity";
Expand All @@ -26,7 +27,7 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {

async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
if (!this.#keyVaultOptions) {
throw new ArgumentError("Failed to process the Key Vault reference because Key Vault options are not configured.");
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
}
let secretIdentifier: KeyVaultSecretIdentifier;
try {
Expand Down
7 changes: 4 additions & 3 deletions src/keyvault/keyVaultSecretProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import { KeyVaultOptions } from "./keyVaultOptions.js";
import { RefreshTimer } from "../refresh/refreshTimer.js";
import { ArgumentError } from "../common/error.js";
import { ArgumentError } from "../common/errors.js";
import { SecretClient, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js";

export class AzureKeyVaultSecretProvider {
#keyVaultOptions: KeyVaultOptions | undefined;
Expand Down Expand Up @@ -51,7 +52,7 @@ export class AzureKeyVaultSecretProvider {

async #getSecretValueFromKeyVault(secretIdentifier: KeyVaultSecretIdentifier): Promise<unknown> {
if (!this.#keyVaultOptions) {
throw new ArgumentError("Failed to get secret value. The keyVaultOptions is not configured.");
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
}
const { name: secretName, vaultUrl, sourceId, version } = secretIdentifier;
// precedence: secret clients > custom secret resolver
Expand All @@ -64,7 +65,7 @@ export class AzureKeyVaultSecretProvider {
return await this.#keyVaultOptions.secretResolver(new URL(sourceId));
}
// When code reaches here, it means that the key vault reference cannot be resolved in all possible ways.
throw new ArgumentError("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_REFERENCE_UNRESOLVABLE);
}

#getSecretClient(vaultUrl: URL): SecretClient | undefined {
Expand Down
11 changes: 6 additions & 5 deletions test/keyvault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const expect = chai.expect;
import { load } from "./exportedApi.js";
import { MAX_TIME_OUT, sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference, sleepInMs } from "./utils/testHelper.js";
import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets";
import { ErrorMessages, KeyVaultReferenceErrorMessages } from "../src/common/errorMessages.js";

const mockedData = [
// key, secretUri, value
Expand Down Expand Up @@ -43,8 +44,8 @@ describe("key vault reference", function () {
try {
await load(createMockedConnectionString());
} catch (error) {
expect(error.message).eq("Failed to load.");
expect(error.cause.message).eq("Failed to process the Key Vault reference because Key Vault options are not configured.");
expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED);
expect(error.cause.message).eq(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
return;
}
// we should never reach here, load should throw an error
Expand Down Expand Up @@ -106,8 +107,8 @@ describe("key vault reference", function () {
}
});
} catch (error) {
expect(error.message).eq("Failed to load.");
expect(error.cause.message).eq("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED);
expect(error.cause.message).eq(KeyVaultReferenceErrorMessages.KEY_VAULT_REFERENCE_UNRESOLVABLE);
return;
}
// we should never reach here, load should throw an error
Expand Down Expand Up @@ -167,7 +168,7 @@ describe("key vault secret refresh", function () {
secretRefreshIntervalInMs: 59999 // less than 60_000 milliseconds
}
});
return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith("The Key Vault secret refresh interval cannot be less than 60000 milliseconds.");
return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith(ErrorMessages.INVALID_SECRET_REFRESH_INTERVAL);
});

it("should reload key vault secret when there is no change to key-values", async () => {
Expand Down
11 changes: 6 additions & 5 deletions test/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ chai.use(chaiAsPromised);
const expect = chai.expect;
import { load } from "./exportedApi.js";
import { MAX_TIME_OUT, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetSnapshot, mockAppConfigurationClientListConfigurationSettingsForSnapshot, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js";
import { ErrorMessages } from "../src/common/errorMessages.js";

const mockedKVs = [{
key: "app.settings.fontColor",
Expand Down Expand Up @@ -164,7 +165,7 @@ describe("load", function () {
snapshotName: "Test",
labelFilter: "\0"
}]
})).eventually.rejectedWith("Key, label or tag filters should not be specified while selecting a snapshot.");
})).eventually.rejectedWith(ErrorMessages.INVALID_SNAPSHOT_SELECTOR);
});

it("should not include feature flags directly in the settings", async () => {
Expand Down Expand Up @@ -359,7 +360,7 @@ describe("load", function () {
labelFilter: "*"
}]
});
return expect(loadWithWildcardLabelFilter).to.eventually.rejectedWith("The characters '*' and ',' are not supported in label filters.");
return expect(loadWithWildcardLabelFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_LABEL_FILTER);
});

it("should not support , in label filters", async () => {
Expand All @@ -370,7 +371,7 @@ describe("load", function () {
labelFilter: "labelA,labelB"
}]
});
return expect(loadWithMultipleLabelFilter).to.eventually.rejectedWith("The characters '*' and ',' are not supported in label filters.");
return expect(loadWithMultipleLabelFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_LABEL_FILTER);
});

it("should throw exception when there is any invalid tag filter", async () => {
Expand All @@ -381,7 +382,7 @@ describe("load", function () {
tagFilters: ["emptyTag"]
}]
});
return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith("Tag filter must follow the format \"tagName=tagValue\"");
return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith(ErrorMessages.INVALID_TAG_FILTER);
});

it("should throw exception when too many tag filters are provided", async () => {
Expand All @@ -404,7 +405,7 @@ describe("load", function () {
}]
});
} catch (error) {
expect(error.message).eq("Failed to load.");
expect(error.message).eq(ErrorMessages.LOAD_OPERATION_FAILED);
expect(error.cause.message).eq("Invalid request parameter 'tags'. Maximum number of tag filters is 5.");
return;
}
Expand Down
Loading