Skip to content

Commit 94bf940

Browse files
authored
[App Config] az appconfig kv set/import: Add support for JSON comments (#32130)
1 parent e2a6fac commit 94bf940

33 files changed

+1061267
-579422
lines changed

src/azure-cli/azure/cli/command_modules/appconfig/_constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ class KeyVaultConstants:
6363
KEYVAULT_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
6464

6565

66+
class AIConfigConstants:
67+
AI_CHAT_COMPLETION_CONTENT_TYPE = "application/vnd.microsoft.appconfig.aichatcompletion+json;charset=utf-8"
68+
69+
6670
class AppServiceConstants:
6771
APPSVC_CONFIG_REFERENCE_PREFIX = "@Microsoft.AppConfiguration"
6872
APPSVC_KEYVAULT_PREFIX = "@Microsoft.KeyVault"
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import json
7+
8+
9+
DOUBLE_QUOTE = '\"'
10+
BACKSLASH = '\\'
11+
DOUBLE_SLASH = '//'
12+
MULTILINE_COMMENT_START = '/*'
13+
MULTILINE_COMMENT_END = '*/'
14+
NEW_LINE = '\n'
15+
16+
17+
def parse_json_with_comments(json_string):
18+
try:
19+
return json.loads(json_string)
20+
21+
except json.JSONDecodeError:
22+
return json.loads(__strip_json_comments(json_string))
23+
24+
25+
def __strip_json_comments(json_string):
26+
current_index = 0
27+
length = len(json_string)
28+
result = []
29+
30+
while current_index < length:
31+
# Single line comment
32+
if json_string[current_index:current_index + 2] == DOUBLE_SLASH:
33+
current_index = __find_next_newline_index(json_string, current_index, length)
34+
if current_index < length and json_string[current_index] == NEW_LINE:
35+
result.append(NEW_LINE)
36+
37+
# Multi-line comment
38+
elif json_string[current_index:current_index + 2] == MULTILINE_COMMENT_START:
39+
current_index = __find_next_multiline_comment_end(json_string, current_index + 2, length)
40+
41+
# String literal
42+
elif json_string[current_index] == DOUBLE_QUOTE:
43+
literal_start_index = current_index
44+
current_index = __find_next_double_quote_index(json_string, current_index + 1, length)
45+
46+
result.extend(json_string[literal_start_index:current_index + 1])
47+
else:
48+
result.append(json_string[current_index])
49+
50+
current_index += 1
51+
52+
return "".join(result)
53+
54+
55+
def __is_escaped(json_string, char_index):
56+
backslash_count = 0
57+
index = char_index - 1
58+
while index >= 0 and json_string[index] == BACKSLASH:
59+
backslash_count += 1
60+
index -= 1
61+
62+
return backslash_count % 2 == 1
63+
64+
65+
def __find_next_newline_index(json_string, start_index, end_index):
66+
index = start_index
67+
68+
while index < end_index:
69+
if json_string[index] == NEW_LINE:
70+
return index
71+
72+
index += 1
73+
74+
return index
75+
76+
77+
def __find_next_double_quote_index(json_string, start_index, end_index):
78+
index = start_index
79+
while index < end_index:
80+
if json_string[index] == DOUBLE_QUOTE and not __is_escaped(json_string, index):
81+
return index
82+
83+
index += 1
84+
85+
raise ValueError("Unterminated string literal")
86+
87+
88+
def __find_next_multiline_comment_end(json_string, start_index, end_index):
89+
index = start_index
90+
while index < end_index - 1:
91+
if json_string[index:index + 2] == MULTILINE_COMMENT_END:
92+
return index + 1
93+
94+
index += 1
95+
96+
raise ValueError("Unterminated multi-line comment")

src/azure-cli/azure/cli/command_modules/appconfig/_kv_import_helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
ImportMode,
4848
)
4949
from ._diff_utils import KVComparer, print_preview
50+
from ._json import parse_json_with_comments
5051
from ._utils import (
5152
is_json_content_type,
5253
validate_feature_flag_name,
@@ -87,7 +88,7 @@ def __read_with_appropriate_encoding(file_path, format_):
8788
try:
8889
with io.open(file_path, "r", encoding=default_encoding) as config_file:
8990
if format_ == "json":
90-
config_data = json.load(config_file)
91+
config_data = parse_json_with_comments(config_file.read())
9192
# Only accept json objects
9293
if not isinstance(config_data, (dict, list)):
9394
raise ValueError(
@@ -112,7 +113,7 @@ def __read_with_appropriate_encoding(file_path, format_):
112113

113114
with io.open(file_path, "r", encoding=detected_encoding) as config_file:
114115
if format_ == "json":
115-
config_data = json.load(config_file)
116+
config_data = parse_json_with_comments(config_file.read())
116117

117118
elif format_ == "yaml":
118119
for yaml_data in list(yaml.safe_load_all(config_file)):

src/azure-cli/azure/cli/command_modules/appconfig/keyvalue.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
)
3636
from knack.log import get_logger
3737
from knack.util import CLIError
38-
from ._constants import HttpHeaders
3938

4039
from azure.appconfiguration import (ConfigurationSetting,
4140
ResourceReadOnlyError)
@@ -50,8 +49,10 @@
5049
from ._constants import (FeatureFlagConstants, KeyVaultConstants,
5150
SearchFilterOptions, StatusCodes,
5251
ImportExportProfiles, CompareFieldsMap,
53-
JsonDiff, ImportMode)
52+
JsonDiff, ImportMode,
53+
AIConfigConstants, HttpHeaders)
5454
from ._featuremodels import map_keyvalue_to_featureflag
55+
from ._json import parse_json_with_comments
5556
from ._models import (convert_configurationsetting_to_keyvalue, convert_keyvalue_to_configurationsetting)
5657
from ._utils import get_appconfig_data_client, prep_filter_for_url_encoding, resolve_store_metadata, get_store_endpoint_from_connection_string, is_json_content_type
5758

@@ -507,9 +508,10 @@ def set_key(cmd,
507508
if retrieved_kv is None:
508509
if is_json_content_type(content_type):
509510
try:
510-
# Ensure that provided value is valid JSON. Error out if value is invalid JSON.
511+
# Ensure that provided value is valid JSON and strip comments if needed.
511512
value = 'null' if value is None else value
512-
json.loads(value)
513+
514+
__validate_json_value(value, content_type)
513515
except ValueError:
514516
raise CLIErrors.ValidationError('Value "{}" is not a valid JSON object, which conflicts with the content type "{}".'.format(value, content_type))
515517

@@ -523,8 +525,8 @@ def set_key(cmd,
523525
content_type = retrieved_kv.content_type if content_type is None else content_type
524526
if is_json_content_type(content_type):
525527
try:
526-
# Ensure that provided/existing value is valid JSON. Error out if value is invalid JSON.
527-
json.loads(value)
528+
# Ensure that provided value is valid JSON and strip comments if needed.
529+
__validate_json_value(value, content_type)
528530
except (TypeError, ValueError):
529531
raise CLIErrors.ValidationError('Value "{}" is not a valid JSON object, which conflicts with the content type "{}". Set the value again in valid JSON format.'.format(value, content_type))
530532
set_kv = ConfigurationSetting(key=key,
@@ -984,3 +986,12 @@ def list_revision(cmd,
984986
return retrieved_revisions
985987
except HttpResponseError as ex:
986988
raise CLIErrors.AzureResponseError('List revision operation failed.\n' + str(ex))
989+
990+
991+
def __validate_json_value(json_string, content_type):
992+
# We do not allow comments in keyvault references, feature flags, and AI chat completion configs
993+
if content_type in (FeatureFlagConstants.FEATURE_FLAG_CONTENT_TYPE, KeyVaultConstants.KEYVAULT_CONTENT_TYPE, AIConfigConstants.AI_CHAT_COMPLETION_CONTENT_TYPE):
994+
json.loads(json_string)
995+
996+
else:
997+
parse_json_with_comments(json_string)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{ // Test single-line backslash comment
2+
"Array": [
3+
"StringBoolValue",
4+
"true",
5+
"StringNullValue",
6+
"null",
7+
"StringNumberValue",
8+
"20.2"
9+
],
10+
/* Test multi-line comment
11+
spanning multiple lines
12+
*/
13+
"ComplexSettings": {
14+
"MyHybridList": [
15+
1000,
16+
true,
17+
"\"EscapedString\"",
18+
"AppConfig string value",
19+
[
20+
"NestedArray",
21+
12,
22+
false,
23+
null
24+
],
25+
{
26+
"ObjectSetting": {
27+
"Logging": {
28+
"LogLevel": "Information",
29+
"Default": "Debug"
30+
}
31+
}
32+
}
33+
],
34+
// Feature management settings
35+
"FeatureManagement": {
36+
"Beta": {
37+
"EnabledFor": [
38+
{
39+
"Name": "1",
40+
"Parameters": {
41+
"q": true,
42+
"w": 243,
43+
"e": null,
44+
"r": [
45+
1,
46+
2,
47+
3
48+
],
49+
"t": [
50+
"a",
51+
"b",
52+
"c"
53+
],
54+
"y": {
55+
"Name": "Value"
56+
},
57+
"u": "string"
58+
}
59+
},
60+
{
61+
"Name": "1"
62+
},
63+
{
64+
"Name": "2",
65+
"Parameters": {
66+
"a": "ss",
67+
"s": 21
68+
}
69+
}
70+
]
71+
},
72+
"DisabledFeature": false,
73+
"EnabledFeature": true
74+
}
75+
}
76+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"Array": [
3+
"StringBoolValue",
4+
"true",
5+
"StringNullValue",
6+
"null",
7+
"StringNumberValue",
8+
"20.2"
9+
],
10+
"ComplexSettings": {
11+
"MyHybridList": [
12+
1000,
13+
true,
14+
"\"EscapedString\"",
15+
"AppConfig string value",
16+
[
17+
"NestedArray",
18+
12,
19+
false,
20+
null
21+
],
22+
{
23+
"ObjectSetting": {
24+
"Logging": {
25+
"LogLevel": "Information",
26+
"Default": "Debug"
27+
}
28+
}
29+
}
30+
],
31+
"FeatureManagement": {
32+
"Beta": {
33+
"EnabledFor": [
34+
{
35+
"Name": "1",
36+
"Parameters": {
37+
"q": true,
38+
"w": 243,
39+
"e": null,
40+
"r": [
41+
1,
42+
2,
43+
3
44+
],
45+
"t": [
46+
"a",
47+
"b",
48+
"c"
49+
],
50+
"y": {
51+
"Name": "Value"
52+
},
53+
"u": "string"
54+
}
55+
},
56+
{
57+
"Name": "1"
58+
},
59+
{
60+
"Name": "2",
61+
"Parameters": {
62+
"a": "ss",
63+
"s": 21
64+
}
65+
}
66+
]
67+
},
68+
"DisabledFeature": false,
69+
"EnabledFeature": true
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)