-
Notifications
You must be signed in to change notification settings - Fork 1
Return configuration changes to caller #21
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
base: main
Are you sure you want to change the base?
Changes from 9 commits
8e39079
ff919a4
bde8c64
5640e71
3dec8f0
d0f60b6
0c44519
be23383
4a70857
8d5f1cd
04e88e4
3c436af
bc799ad
0b15321
5f9c46a
0be1539
f2c0fc5
30f8995
7b7c06d
2abfdbd
7740afe
3fbdab5
e76d439
adad73b
9dda16d
963008d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |
| import { ImportMode } from "./enums"; | ||
| import { OperationTimeoutError, ArgumentError } from "./errors"; | ||
| import { AdaptiveTaskManager } from "./internal/adaptiveTaskManager"; | ||
| import { ImportProgress, KeyLabelLookup } from "./models"; | ||
| import { ImportProgress, KeyLabelLookup, ConfigurationChanges } from "./models"; | ||
| import { isConfigSettingEqual } from "./internal/utils"; | ||
| import { v4 as uuidv4 } from "uuid"; | ||
| import { Constants } from "./internal/constants"; | ||
|
|
@@ -43,27 +43,74 @@ | |
| * @param timeout - Seconds of entire import progress timeout | ||
| * @param progressCallback - Callback for report the progress of import | ||
| * @param importMode - Determines the behavior when importing key-values. The default value, 'All' will import all key-values in the input file to App Configuration. 'Ignore-Match' will only import settings that have no matching key-value in App Configuration. | ||
ChristineWanjau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @param dryRun - When dry run is enabled, no updates will be performed to App Configuration. Instead, any updates that would have been performed in a normal run will be printed to the console for review | ||
| * @returns Promise<void> | ||
| */ | ||
| public async Import( | ||
| configSettingsSource: ConfigurationSettingsSource, | ||
| timeout: number, | ||
| strict = false, | ||
| progressCallback?: (progress: ImportProgress) => unknown, | ||
| importMode?: ImportMode | ||
MaryanneNjeri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ): Promise<void> { | ||
| if (importMode === undefined) { | ||
| importMode = ImportMode.IgnoreMatch; | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] Unnecessary blank line |
||
| this.validateImportMode(importMode); | ||
MaryanneNjeri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Generate correlation ID for operations | ||
ChristineWanjau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const customCorrelationRequestId: string = uuidv4(); | ||
| const customHeadersOption: OperationOptions = { | ||
| requestOptions: { | ||
| customHeaders: { | ||
| [Constants.CorrelationRequestIdHeader]: customCorrelationRequestId | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const configurationChanges: ConfigurationChanges = await this.GetConfigurationChanges(configSettingsSource, strict, importMode, customHeadersOption); | ||
|
||
|
|
||
| await this.applyUpdatesToServer([...configurationChanges.Added, ...configurationChanges.Modified], configurationChanges.Deleted, timeout, customHeadersOption, progressCallback); | ||
| } | ||
|
|
||
| /** | ||
| * Get configuration changes between source settings and Azure App Configuration service without any applying changes | ||
MaryanneNjeri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * Example usage: | ||
| * ```ts | ||
| * const fileData = fs.readFileSync("mylocalPath").toString(); | ||
| * const configurationChanges = await client.getConfigurationChanges( | ||
ChristineWanjau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * new StringConfigurationSettingsSource({data:fileData, format: ConfigurationFormat.Json}), | ||
| * false, | ||
| * ImportMode.All, | ||
| * options | ||
| * ); | ||
| * ``` | ||
| * @param configSettingsSource - A ConfigurationSettingsSource instance. | ||
| * @param strict - Use strict mode to delete settings not in source. | ||
| * @param importMode - Determines the behavior when analyzing key-values. | ||
| * 'All' will include all key-values. | ||
| * 'Ignore-Match' will exclude settings that have matching key-values in App Configuration. | ||
| * @param customHeadersOption - Custom headers for the operation. | ||
| * @returns ConfigurationChanges object containing Added, Modified, and Deleted settings | ||
| */ | ||
| public async GetConfigurationChanges( | ||
| configSettingsSource: ConfigurationSettingsSource, | ||
| strict = false, | ||
| importMode?: ImportMode, | ||
| dryRun?: boolean | ||
| ) { | ||
| customHeadersOption?: OperationOptions | ||
RichardChen820 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ): Promise<ConfigurationChanges> { | ||
| if (importMode == undefined) { | ||
| importMode = ImportMode.IgnoreMatch; | ||
| } | ||
| if (dryRun == undefined) { | ||
| dryRun = false; | ||
| } | ||
|
|
||
| this.validateImportMode(importMode); | ||
|
|
||
| const configSettings = await configSettingsSource.GetConfigurationSettings(); | ||
|
|
||
| const configurationSettingToDelete: ConfigurationSetting<string>[] = []; | ||
| const configurationSettingToModify: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[] = []; | ||
| const configurationSettingToAdd: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[] = []; | ||
| const srcKeyLabelLookUp: KeyLabelLookup = {}; | ||
|
|
||
| configSettings.forEach((config: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>) => { | ||
|
|
@@ -73,59 +120,43 @@ | |
| srcKeyLabelLookUp[config.key][config.label || ""] = true; | ||
| }); | ||
|
|
||
| // generate correlationRequestId for operations in the same activity | ||
| const customCorrelationRequestId: string = uuidv4(); | ||
| const customHeadersOption: OperationOptions = { | ||
| requestOptions: { | ||
| customHeaders: { | ||
| [Constants.CorrelationRequestIdHeader]: customCorrelationRequestId | ||
| } | ||
| } | ||
| }; | ||
| configurationSettingToAdd.push(...configSettings); | ||
|
|
||
| if (strict || importMode == ImportMode.IgnoreMatch) { | ||
| for await (const existing of this.configurationClient.listConfigurationSettings({...configSettingsSource.FilterOptions, ...customHeadersOption})) { | ||
|
|
||
| const isKeyLabelPresent: boolean = srcKeyLabelLookUp[existing.key] && srcKeyLabelLookUp[existing.key][existing.label || ""]; | ||
| for await (const existing of this.configurationClient.listConfigurationSettings({...configSettingsSource.FilterOptions, ...customHeadersOption})) { | ||
| const isKeyLabelPresent: boolean = srcKeyLabelLookUp[existing.key] && srcKeyLabelLookUp[existing.key][existing.label || ""]; | ||
| if (strict && !isKeyLabelPresent) { | ||
| configurationSettingToDelete.push(existing); | ||
| } | ||
|
|
||
| const incoming = configSettings.find(configSetting => configSetting.key == existing.key && | ||
ChristineWanjau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
ChristineWanjau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| configSetting.label === existing.label); | ||
|
|
||
| if (incoming) { | ||
ChristineWanjau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const settingsAreEqual: boolean = isConfigSettingEqual(incoming, existing); | ||
|
|
||
| if (strict && !isKeyLabelPresent) { | ||
| configurationSettingToDelete.push(existing); | ||
| if (!settingsAreEqual) { | ||
| configurationSettingToModify.push(incoming); | ||
| // Remove from add list since it's a modification, not an addition | ||
| const addIndex = configurationSettingToAdd.indexOf(incoming); | ||
MaryanneNjeri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (addIndex !== -1) { | ||
| configurationSettingToAdd.splice(addIndex, 1); | ||
| } | ||
| } | ||
|
|
||
| if (importMode == ImportMode.IgnoreMatch) { | ||
| const incoming = configSettings.find(configSetting => configSetting.key == existing.key && | ||
| configSetting.label == existing.label); | ||
|
|
||
| if (incoming && isConfigSettingEqual(incoming, existing)) { | ||
| configSettings.splice(configSettings.indexOf(incoming), 1); | ||
| else if (importMode === ImportMode.IgnoreMatch) { | ||
| // Remove unchanged settings from add list | ||
| const addIndex = configurationSettingToAdd.indexOf(incoming); | ||
| if (addIndex !== -1) { | ||
| configurationSettingToAdd.splice(addIndex, 1); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (dryRun) { | ||
| this.printUpdatesToConsole(configSettings, configurationSettingToDelete); | ||
| } | ||
| else { | ||
| await this.applyUpdatesToServer(configSettings, configurationSettingToDelete, timeout, customHeadersOption, progressCallback); | ||
| } | ||
| } | ||
|
|
||
| private printUpdatesToConsole( | ||
| settingsToAdd: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[], | ||
| settingsToDelete: ConfigurationSetting<string>[] | ||
| ): void { | ||
| console.log("The following settings will be removed from App Configuration:"); | ||
| for (const setting of settingsToDelete) { | ||
|
|
||
| console.log(JSON.stringify({key: setting.key, label: setting.label, contentType: setting.contentType, tags: setting.tags})); | ||
| } | ||
|
|
||
| console.log("\nThe following settings will be written to App Configuration:"); | ||
| for (const setting of settingsToAdd) { | ||
|
|
||
| console.log(JSON.stringify({key: setting.key, label: setting.label, contentType: setting.contentType, tags: setting.tags})); | ||
| } | ||
| return { | ||
| Added: configurationSettingToAdd, | ||
| Modified: configurationSettingToModify, | ||
| Deleted: configurationSettingToDelete | ||
| }; | ||
| } | ||
|
|
||
| private async applyUpdatesToServer( | ||
RichardChen820 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
@@ -146,7 +177,7 @@ | |
| await this.executeTasksWithTimeout(importTaskManager, timeout, progressCallback); | ||
| } | ||
|
|
||
| private newAdaptiveTaskManager<T>(task: (setting: T) => Promise<any>, configurationSettings: Array<T>) { | ||
| let index = 0; | ||
| return new AdaptiveTaskManager(() => { | ||
| if (index == configurationSettings.length) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,13 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| import { | ||
| SecretReferenceValue, | ||
| ConfigurationSetting, | ||
| SetConfigurationSettingParam, | ||
| FeatureFlagValue | ||
| } from "@azure/app-configuration"; | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
|
|
@@ -42,4 +49,10 @@ export interface KeyLabelLookup { | |
| [key: string]: { | ||
| [label: string] : boolean | ||
| } | ||
| } | ||
|
|
||
| export interface ConfigurationChanges { | ||
MaryanneNjeri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Deleted: ConfigurationSetting<string>[]; | ||
| Modified: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[]; | ||
| Added: SetConfigurationSettingParam<string | FeatureFlagValue | SecretReferenceValue>[]; | ||
ChristineWanjau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In portal how will we distinguish key values that haven't changed when import mode is All ? I guess I was expecting an additional optional property for key values that haven't changed
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If they haven't changed and import mode is All, they will be under modified. Could you elaborate more on the additional optional property? Not sure I follow
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usualy we have different icons to differentiate if a kv is added, deleted, modified or no change, icon for no change and modified is different if import mode All and kvs that haven't changed will be under modified how will we differentiate them from kvs that are modified in portal ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if a key-value hasn't changed and import mode is ALL. We write the key-value again which means the etag will change. In my opinion it still counts as a modified key-value.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the update status for kv that isn't changed we show the status as "Equal" or "Updated" ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From Portal, I do understand that it might be confusing to see "Updated" for a key-value that hasn't changed. Although only the etag would change, this is not visible to the user. Probably the user just cares about the key, label, value and content type, tag properties. Although, I am wondering if we show equal in the diff on portal, if it will still be confusing if those key-values will still be written. What do you think? Should we have another category in configuration changes to differentiate between key-values who have actually been modified (value, content-type and tags) and key-values to be refreshed (unchanged but will have a new etag)?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should have a category for unchanged key value also based on the current design you have, etag is more of a version identifier for a key and its updated anytime the kv is modified. Even in the diffs we currently show etag is not part of the diff its the other kv properties.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why we need to differentiate them? If ImportMode is All, as long as this key-value exists, we will always put a new revision for it, whether its value has changed or not.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||


Uh oh!
There was an error while loading. Please reload this page.