Skip to content

Commit 070f2c2

Browse files
Small General Sample Addition to obtain and log an API ID (#91)
1 parent 179ea69 commit 070f2c2

File tree

9 files changed

+116
-34
lines changed

9 files changed

+116
-34
lines changed

samples/general/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ Sets up a simple APIM instance with a variety of policies to experiment.
1212
1. Become proficient with how policies operate.
1313
1. Gain confidence in setting up and configuring policies appropriately.
1414

15+
## 🔗 APIs
16+
17+
| API Name | What does it do? |
18+
|:------------------------------|:----------------------------------------------------------------------------------------------------------------------------------|
19+
| Request Headers | Returns a list of the request headers sent to APIM. Useful for debugging. |
20+
| API ID | Returns the ID of an API as per a predefined standard such as `api-123`. Useful for explicitly identifying an API in telemetry. |
21+
1522
## ⚙️ Configuration
1623

1724
1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use.
18-
1. If the infrastructure _does not_ yet exist, navigate to the desired [infrastructure](../../infrastructure/) folder and follow its README.md.
19-
1. If the infrastructure _does_ exist, adjust the `user-defined parameters` in the _Initialize notebook variables_ below. Please ensure that all parameters match your infrastructure.
25+
1. Press `Run All` in this sample's `create.ipynb` notebook.

samples/general/create.ipynb

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,20 @@
4242
"\n",
4343
"# Define the APIs and their operations and policies\n",
4444
"\n",
45-
"# API 1\n",
46-
"api1_path = f'{api_prefix}api1'\n",
47-
"api1_get = GET_APIOperation('This is a GET for API 1')\n",
48-
"api1_post = POST_APIOperation('This is a POST for API 1')\n",
49-
"api1 = API(api1_path, 'API 1', api1_path, 'This is API 1', operations = [api1_get, api1_post], tags = tags)\n",
50-
"\n",
51-
"# API 2\n",
52-
"api2_path = f'{api_prefix}api2'\n",
53-
"api2_post = POST_APIOperation('This is a POST for API 2')\n",
54-
"api2 = API(api2_path, 'API 2', api2_path, 'This is API 2', operations = [api2_post], tags = tags)\n",
55-
"\n",
56-
"# API 3: Request Headers\n",
45+
"# API 1: Request Headers\n",
5746
"rh_path = f'{api_prefix}request-headers'\n",
5847
"pol_request_headers_get = utils.read_policy_xml(REQUEST_HEADERS_XML_POLICY_PATH)\n",
5948
"request_headers_get = GET_APIOperation('Gets the request headers for the current request and returns them. Great for troubleshooting.', pol_request_headers_get)\n",
6049
"request_headers = API(rh_path, 'Request Headers', rh_path, 'API for request headers', operations = [request_headers_get], tags = tags)\n",
6150
"\n",
51+
"# API 2: API ID: Extract and trace API Identifier\n",
52+
"api_id_path = f'{api_prefix}api-id'\n",
53+
"pol_api_id_get = utils.read_policy_xml(API_ID_XML_POLICY_PATH)\n",
54+
"api_id_get = GET_APIOperation('Gets the API identifier for the current request and traces it', pol_api_id_get)\n",
55+
"api_id = API(api_id_path, 'API Identifier (api-42)', api_id_path, 'API for extracting and tracing API identifier', operations = [api_id_get], tags = tags)\n",
56+
"\n",
6257
"# APIs Array\n",
63-
"apis: List[API] = [api1, api2, request_headers]\n",
58+
"apis: List[API] = [request_headers, api_id]\n",
6459
"\n",
6560
"utils.print_ok('Notebook initialized')"
6661
]
@@ -121,16 +116,22 @@
121116
"\n",
122117
"# Initialize testing framework\n",
123118
"tests = ApimTesting(\"General Sample Tests\", sample_folder, nb_helper.deployment)\n",
124-
"api_subscription_key = apim_apis[2]['subscriptionPrimaryKey']\n",
125119
"\n",
126120
"# Preflight: Check if the infrastructure architecture deployment uses Azure Front Door. If so, assume that APIM is not directly accessible and use the Front Door URL instead.\n",
127121
"endpoint_url = utils.test_url_preflight_check(deployment, rg_name, apim_gateway_url)\n",
128122
"\n",
129123
"# 1) Request Headers\n",
124+
"api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n",
130125
"reqs = ApimRequests(endpoint_url, api_subscription_key)\n",
131-
"output = reqs.singleGet('/request-headers', msg = 'Calling Request Headers API via Azure Front Door. Expect 200.')\n",
126+
"output = reqs.singleGet('/request-headers', msg = 'Calling the Request Headers API. Expect 200.')\n",
132127
"tests.verify('Host:' in output, True)\n",
133128
"\n",
129+
"# 2) API ID\n",
130+
"api_subscription_key = apim_apis[1]['subscriptionPrimaryKey']\n",
131+
"reqs = ApimRequests(endpoint_url, api_subscription_key)\n",
132+
"output = reqs.singleGet('/api-id', msg = 'Calling the API ID API. Expect 200.')\n",
133+
"tests.verify(output, 'Extracted API ID: api-42')\n",
134+
"\n",
134135
"tests.print_summary()\n",
135136
"\n",
136137
"utils.print_ok('All done!')"

shared/apim-policies/api-id.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
This policy sample demonstrates the listing of all inbound HTTP headers, which can be a bit more straight-forward than setting up a trace.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<include-fragment fragment-id="Api-Id" />
8+
<return-response>
9+
<set-status code="200" reason="OK" />
10+
<set-body>
11+
@{
12+
var apiId = context.Variables.GetValueOrDefault<string>("apiId", string.Empty);
13+
return (string.IsNullOrEmpty(apiId)) ? "No API ID extracted." : string.Format("Extracted API ID: {0}", apiId);
14+
}
15+
</set-body>
16+
</return-response>
17+
</inbound>
18+
<backend>
19+
<base />
20+
</backend>
21+
<outbound>
22+
<base />
23+
</outbound>
24+
<on-error>
25+
<base />
26+
</on-error>
27+
</policies>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!--
2+
Policy Fragment: Extract API ID from API Name
3+
4+
Tags are very useful to explicitly identify an API as API names or paths may be ambiguous and repetitive. Having explicit IDs can significantly aid with telemetry.
5+
As there is no way yet to extract a tag associated with an API in Azure API Management using policies, putting such information in the API name is a workable alternative.
6+
If the format is consistent, it can be parsed and used in the policies.
7+
8+
The format we are looking for is `<api-name> (api-246)`, where the value in parentheses starts with "api-" followed by numeric characters.
9+
10+
This fragment extracts the API ID from context.Api.Name and stores it in the "apiId" variable.
11+
If the format doesn't match the expected pattern, the variable will be set to an empty string.
12+
13+
Expected format examples:
14+
- "User Management API (api-246)" → extracts "api-246"
15+
- "Order Service (api-123)" → extracts "api-123"
16+
- "Payment Gateway (payment-456)" → ignored (doesn't start with "api-")
17+
- "Simple API" → ignored (no parentheses)
18+
19+
https://learn.microsoft.com/azure/api-management/api-management-policy-expressions#ref-context-api
20+
-->
21+
<fragment>
22+
<set-variable name="apiId" value="@{
23+
// Use regex to match the pattern: (api-\d+) at the end of the string
24+
var match = System.Text.RegularExpressions.Regex.Match(context.Api.Name, @"\(api-\d+\)$");
25+
26+
// Extract the content within parentheses, removing the parentheses
27+
return (match.Success) ? match.Value.Substring(1, match.Value.Length - 2) : string.Empty;
28+
}" />
29+
30+
<choose>
31+
<when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("apiId", string.Empty)))">
32+
<trace source="API Id Policy Fragment">
33+
<message>@{
34+
return "Extracted API ID: " + (string)context.Variables["apiId"];
35+
}</message>
36+
<metadata name="ApiId" value="@((string)context.Variables["apiId"])" />
37+
</trace>
38+
<!-- Set a header for downstream services to use -->
39+
<set-header name="X-Api-Id" exists-action="override">
40+
<value>@((string)context.Variables["apiId"])</value>
41+
</set-header>
42+
</when>
43+
</choose>
44+
</fragment>

shared/python/apimtesting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def verify(self, value: any, expected: any) -> bool:
5151
bool: True, if the assertion passes; otherwise, False.
5252
"""
5353
try:
54+
print(f'🔍 Assert that [{value}] matches [{expected}].')
5455
self.total_tests += 1
5556
assert value == expected, f'Value [{value}] does not match expected [{expected}]'
5657
self.tests_passed += 1

shared/python/apimtypes.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ def _get_project_root() -> Path:
3636
_SHARED_XML_POLICY_BASE_PATH = _PROJECT_ROOT / 'shared' / 'apim-policies'
3737

3838
# Policy file paths (now absolute and platform-independent)
39-
DEFAULT_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'default.xml')
40-
HELLO_WORLD_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'hello-world.xml')
41-
REQUEST_HEADERS_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'request-headers.xml')
42-
BACKEND_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'backend.xml')
39+
DEFAULT_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'default.xml')
40+
HELLO_WORLD_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'hello-world.xml')
41+
REQUEST_HEADERS_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'request-headers.xml')
42+
BACKEND_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'backend.xml')
43+
API_ID_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'api-id.xml')
4344

4445
SUBSCRIPTION_KEY_PARAMETER_NAME = 'api-key'
4546
SLEEP_TIME_BETWEEN_REQUESTS_MS = 50

shared/python/infrastructures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def _define_policy_fragments(self) -> List[PolicyFragment]:
5959

6060
# The base policy fragments common to all infrastructures
6161
self.base_pfs = [
62+
PolicyFragment('Api-Id', utils.read_policy_xml(utils.determine_shared_policy_path('pf-api-id.xml')), 'Extracts a specific API identifier for tracing.'),
6263
PolicyFragment('AuthZ-Match-All', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-all.xml')), 'Authorizes if all of the specified roles match the JWT role claims.'),
6364
PolicyFragment('AuthZ-Match-Any', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-any.xml')), 'Authorizes if any of the specified roles match the JWT role claims.'),
6465
PolicyFragment('Http-Response-200', utils.read_policy_xml(utils.determine_shared_policy_path('pf-http-response-200.xml')), 'Returns a 200 OK response for the current HTTP method.'),

shared/python/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def _query_and_select_infrastructure(self) -> tuple[INFRASTRUCTURE | None, int |
473473
option_counter += 1
474474
else:
475475
print_warning('No existing supported infrastructures found.')
476-
print_info(f'🚀 Automatically proceeding to create new infrastructure: {self.deployment.value}')
476+
print_info(f'Automatically proceeding to create new infrastructure: {self.deployment.value}')
477477

478478
# Automatically create the desired infrastructure without user confirmation
479479
selected_index = self._get_current_index()

tests/python/test_infrastructures.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def test_infrastructure_creation_with_custom_policy_fragments(mock_utils, mock_p
116116
pfs = infra._define_policy_fragments()
117117

118118
# Should have base policy fragments + custom ones
119-
assert len(pfs) == 7 # 5 base + 2 custom
119+
assert len(pfs) == 8 # 6 base + 2 custom
120120
assert any(pf.name == 'Test-Fragment-1' for pf in pfs)
121121
assert any(pf.name == 'Test-Fragment-2' for pf in pfs)
122122
assert any(pf.name == 'AuthZ-Match-All' for pf in pfs)
@@ -225,8 +225,8 @@ def test_define_policy_fragments_with_none_input(mock_utils):
225225
pfs = infra._define_policy_fragments()
226226

227227
# Should only have base policy fragments
228-
assert len(pfs) == 5
229-
assert all(pf.name in ['AuthZ-Match-All', 'AuthZ-Match-Any', 'Http-Response-200', 'Product-Match-Any', 'Remove-Request-Headers'] for pf in pfs)
228+
assert len(pfs) == 6
229+
assert all(pf.name in ['Api-Id', 'AuthZ-Match-All', 'AuthZ-Match-Any', 'Http-Response-200', 'Product-Match-Any', 'Remove-Request-Headers'] for pf in pfs)
230230

231231
@pytest.mark.unit
232232
def test_define_policy_fragments_with_custom_input(mock_utils, mock_policy_fragments):
@@ -242,7 +242,7 @@ def test_define_policy_fragments_with_custom_input(mock_utils, mock_policy_fragm
242242
pfs = infra._define_policy_fragments()
243243

244244
# Should have base + custom policy fragments
245-
assert len(pfs) == 7 # 5 base + 2 custom
245+
assert len(pfs) == 8 # 6 base + 2 custom
246246
fragment_names = [pf.name for pf in infra.pfs]
247247
assert 'Test-Fragment-1' in fragment_names
248248
assert 'Test-Fragment-2' in fragment_names
@@ -319,7 +319,7 @@ def test_define_bicep_parameters(mock_utils):
319319

320320
assert 'policyFragments' in bicep_params
321321
assert isinstance(bicep_params['policyFragments']['value'], list)
322-
assert len(bicep_params['policyFragments']['value']) == 5 # base policy fragments
322+
assert len(bicep_params['policyFragments']['value']) == 6 # base policy fragments
323323

324324

325325
# ------------------------------
@@ -769,16 +769,16 @@ def test_infrastructure_end_to_end_simple(mock_utils):
769769

770770
# Verify all components are created correctly
771771
assert infra.infra == INFRASTRUCTURE.SIMPLE_APIM
772-
assert len(infra.base_pfs) == 5
773-
assert len(infra.pfs) == 5
772+
assert len(infra.base_pfs) == 6
773+
assert len(infra.pfs) == 6
774774
assert len(infra.base_apis) == 1
775775
assert len(infra.apis) == 1
776776

777777
# Verify bicep parameters
778778
bicep_params = infra._define_bicep_parameters()
779779
assert bicep_params['apimSku']['value'] == 'Developer'
780780
assert len(bicep_params['apis']['value']) == 1
781-
assert len(bicep_params['policyFragments']['value']) == 5
781+
assert len(bicep_params['policyFragments']['value']) == 6
782782

783783
@pytest.mark.unit
784784
def test_infrastructure_with_all_custom_components(mock_utils, mock_policy_fragments, mock_apis):
@@ -798,16 +798,16 @@ def test_infrastructure_with_all_custom_components(mock_utils, mock_policy_fragm
798798
infra._define_apis()
799799

800800
# Verify all components are combined correctly
801-
assert len(infra.base_pfs) == 5
802-
assert len(infra.pfs) == 7 # 5 base + 2 custom
801+
assert len(infra.base_pfs) == 6
802+
assert len(infra.pfs) == 8 # 6 base + 2 custom
803803
assert len(infra.base_apis) == 1
804804
assert len(infra.apis) == 3 # 1 base + 2 custom
805805

806806
# Verify bicep parameters include all components
807807
bicep_params = infra._define_bicep_parameters()
808808
assert bicep_params['apimSku']['value'] == 'Premium'
809809
assert len(bicep_params['apis']['value']) == 3
810-
assert len(bicep_params['policyFragments']['value']) == 7
810+
assert len(bicep_params['policyFragments']['value']) == 8
811811

812812

813813
# ------------------------------
@@ -856,7 +856,7 @@ def test_infrastructure_empty_custom_lists(mock_utils):
856856
infra._define_apis()
857857

858858
# Empty lists should behave the same as None
859-
assert len(infra.pfs) == 5 # Only base policy fragments
859+
assert len(infra.pfs) == 6 # Only base policy fragments
860860
assert len(infra.apis) == 1 # Only base APIs
861861

862862
@pytest.mark.unit
@@ -927,6 +927,7 @@ def test_policy_fragment_creation_robustness(mock_utils):
927927
'<policy3/>',
928928
'<policy4/>',
929929
'<policy5/>',
930+
'<policy6/>', # Added for the new Api-Id policy fragment
930931
'<hello-world-policy/>'
931932
]
932933

0 commit comments

Comments
 (0)